1
# -*- coding: utf-8 -*-
2 84
from __future__ import unicode_literals
3 84
from ._common import PicklableMixin
4 84
from ._common import TZEnvContext, TZWinContext
5 84
from ._common import ComparesEqual
6

7 84
from datetime import datetime, timedelta
8 84
from datetime import time as dt_time
9 84
from datetime import tzinfo
10 84
from six import PY2
11 84
from io import BytesIO, StringIO
12 84
import unittest
13

14 84
import sys
15 84
import base64
16 84
import copy
17 84
import gc
18 84
import weakref
19

20 84
from functools import partial
21

22 84
IS_WIN = sys.platform.startswith('win')
23

24 84
import pytest
25

26
# dateutil imports
27 84
from dateutil.relativedelta import relativedelta, SU, TH
28 84
from dateutil.parser import parse
29 84
from dateutil import tz as tz
30 84
from dateutil import zoneinfo
31

32 84
try:
33 84
    from dateutil import tzwin
34 75
except ImportError as e:
35 75
    if IS_WIN:
36 0
        raise e
37
    else:
38 75
        pass
39

40 84
MISSING_TARBALL = ("This test fails if you don't have the dateutil "
41
                   "timezone file installed. Please read the README")
42

43 84
TZFILE_EST5EDT = b"""
44
VFppZgAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAADrAAAABAAAABCeph5wn7rrYKCGAHCh
45
ms1gomXicKOD6eCkaq5wpTWnYKZTyvCnFYlgqDOs8Kj+peCqE47wqt6H4KvzcPCsvmngrdNS8K6e
46
S+CvszTwsH4t4LGcUXCyZ0pgs3wzcLRHLGC1XBVwticOYLc793C4BvBguRvZcLnm0mC7BPXwu8a0
47
YLzk1/C9r9DgvsS58L+PsuDApJvwwW+U4MKEffDDT3bgxGRf8MUvWODGTXxwxw864MgtXnDI+Fdg
48
yg1AcMrYOWDLiPBw0iP0cNJg++DTdeTw1EDd4NVVxvDWIL/g1zWo8NgAoeDZFYrw2eCD4Nr+p3Db
49
wGXg3N6JcN2pgmDevmtw34lkYOCeTXDhaUZg4n4vcONJKGDkXhFw5Vcu4OZHLfDnNxDg6CcP8OkW
50
8uDqBvHw6vbU4Ovm0/Ds1rbg7ca18O6/02Dvr9Jw8J+1YPGPtHDyf5dg82+WcPRfeWD1T3hw9j9b
51
YPcvWnD4KHfg+Q88cPoIWeD6+Fjw++g74PzYOvD9yB3g/rgc8P+n/+AAl/7wAYfh4AJ34PADcP5g
52
BGD9cAVQ4GAGQN9wBzDCYAeNGXAJEKRgCa2U8ArwhmAL4IVwDNmi4A3AZ3AOuYTgD6mD8BCZZuAR
53
iWXwEnlI4BNpR/AUWSrgFUkp8BY5DOAXKQvwGCIpYBkI7fAaAgtgGvIKcBvh7WAc0exwHcHPYB6x
54
znAfobFgIHYA8CGBk2AiVeLwI2qv4CQ1xPAlSpHgJhWm8Ccqc+An/sNwKQpV4CnepXAq6jfgK76H
55
cCzTVGAtnmlwLrM2YC9+S3AwkxhgMWdn8DJy+mAzR0nwNFLcYDUnK/A2Mr5gNwcN8Dgb2uA45u/w
56
Ofu84DrG0fA7257gPK/ucD27gOA+j9BwP5ti4EBvsnBBhH9gQk+UcENkYWBEL3ZwRURDYEYPWHBH
57
JCVgR/h08EkEB2BJ2FbwSuPpYEu4OPBMzQXgTZga8E6s5+BPd/zwUIzJ4FFhGXBSbKvgU0D7cFRM
58
jeBVIN1wVixv4FcAv3BYFYxgWOChcFn1bmBawINwW9VQYFypn/BdtTJgXomB8F+VFGBgaWPwYX4w
59
4GJJRfBjXhLgZCkn8GU99OBmEkRwZx3W4GfyJnBo/bjgadIIcGrdmuBrsepwbMa3YG2RzHBupplg
60
b3GucHCGe2BxWsrwcmZdYHM6rPB0Rj9gdRqO8HYvW+B2+nDweA894HjaUvB57x/gero08HvPAeB8
61
o1Fwfa7j4H6DM3B/jsXgAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB
62
AAEAAQABAgMBAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB
63
AAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEA
64
AQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB
65
AAEAAQABAAEAAQABAAEAAQABAAEAAf//x8ABAP//ubAABP//x8ABCP//x8ABDEVEVABFU1QARVdU
66
AEVQVAAAAAABAAAAAQ==
67
"""
68

69 84
EUROPE_HELSINKI = b"""
70
VFppZgAAAAAAAAAAAAAAAAAAAAAAAAAFAAAABQAAAAAAAAB1AAAABQAAAA2kc28Yy85RYMy/hdAV
71
I+uQFhPckBcDzZAX876QGOOvkBnToJAaw5GQG7y9EBysrhAdnJ8QHoyQEB98gRAgbHIQIVxjECJM
72
VBAjPEUQJCw2ECUcJxAmDBgQJwVDkCf1NJAo5SWQKdUWkCrFB5ArtPiQLKTpkC2U2pAuhMuQL3S8
73
kDBkrZAxXdkQMnK0EDM9uxA0UpYQNR2dEDYyeBA2/X8QOBuUkDjdYRA5+3aQOr1DEDvbWJA8pl+Q
74
Pbs6kD6GQZA/mxyQQGYjkEGEORBCRgWQQ2QbEEQl55BFQ/0QRgXJkEcj3xBH7uYQSQPBEEnOyBBK
75
46MQS66qEEzMv5BNjowQTqyhkE9ubhBQjIOQUVeKkFJsZZBTN2yQVExHkFUXTpBWLCmQVvcwkFgV
76
RhBY1xKQWfUoEFq29JBb1QoQXKAREF207BBef/MQX5TOEGBf1RBhfeqQYj+3EGNdzJBkH5kQZT2u
77
kGYItZBnHZCQZ+iXkGj9cpBpyHmQat1UkGuoW5BsxnEQbYg9kG6mUxBvaB+QcIY1EHFRPBByZhcQ
78
czEeEHRF+RB1EQAQdi8VkHbw4hB4DveQeNDEEHnu2ZB6sKYQe867kHyZwpB9rp2QfnmkkH+Of5AC
79
AQIDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQD
80
BAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAME
81
AwQAABdoAAAAACowAQQAABwgAAkAACowAQQAABwgAAlITVQARUVTVABFRVQAAAAAAQEAAAABAQ==
82
"""
83

84 84
NEW_YORK = b"""
85
VFppZgAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAABcAAADrAAAABAAAABCeph5wn7rrYKCGAHCh
86
ms1gomXicKOD6eCkaq5wpTWnYKZTyvCnFYlgqDOs8Kj+peCqE47wqt6H4KvzcPCsvmngrdNS8K6e
87
S+CvszTwsH4t4LGcUXCyZ0pgs3wzcLRHLGC1XBVwticOYLc793C4BvBguRvZcLnm0mC7BPXwu8a0
88
YLzk1/C9r9DgvsS58L+PsuDApJvwwW+U4MKEffDDT3bgxGRf8MUvWODGTXxwxw864MgtXnDI+Fdg
89
yg1AcMrYOWDLiPBw0iP0cNJg++DTdeTw1EDd4NVVxvDWIL/g1zWo8NgAoeDZFYrw2eCD4Nr+p3Db
90
wGXg3N6JcN2pgmDevmtw34lkYOCeTXDhaUZg4n4vcONJKGDkXhFw5Vcu4OZHLfDnNxDg6CcP8OkW
91
8uDqBvHw6vbU4Ovm0/Ds1rbg7ca18O6/02Dvr9Jw8J+1YPGPtHDyf5dg82+WcPRfeWD1T3hw9j9b
92
YPcvWnD4KHfg+Q88cPoIWeD6+Fjw++g74PzYOvD9yB3g/rgc8P+n/+AAl/7wAYfh4AJ34PADcP5g
93
BGD9cAVQ4GEGQN9yBzDCYgeNGXMJEKRjCa2U9ArwhmQL4IV1DNmi5Q3AZ3YOuYTmD6mD9xCZZucR
94
iWX4EnlI6BNpR/kUWSrpFUkp+RY5DOoXKQv6GCIpaxkI7fsaAgtsGvIKfBvh7Wwc0ex8HcHPbR6x
95
zn0fobFtIHYA/SGBk20iVeL+I2qv7iQ1xP4lSpHuJhWm/ycqc+8n/sOAKQpV8CnepYAq6jfxK76H
96
gSzTVHItnmmCLrM2cy9+S4MwkxhzMWdoBDJy+nQzR0oENFLcdTUnLAU2Mr51NwcOBjgb2vY45vAG
97
Ofu89jrG0gY72572PK/uhj27gPY+j9CGP5ti9kBvsoZBhH92Qk+UhkNkYXZEL3aHRURDd0XzqQdH
98
LV/3R9OLB0kNQfdJs20HSu0j90uciYdM1kB3TXxrh062IndPXE2HUJYEd1E8L4dSdeZ3UxwRh1RV
99
yHdU+/OHVjWqd1blEAdYHsb3WMTyB1n+qPdapNQHW96K91yEtgddvmz3XmSYB1+eTvdgTbSHYYdr
100
d2ItlodjZ013ZA14h2VHL3dl7VqHZycRd2fNPIdpBvN3aa0eh2rm1XdrljsHbM/x9212HQdur9P3
101
b1X/B3CPtfdxNeEHcm+X93MVwwd0T3n3dP7fh3Y4lnd23sGHeBh4d3i+o4d5+Fp3ep6Fh3vYPHd8
102
fmeHfbged35eSYd/mAB3AAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB
103
AAEAAQABAgMBAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB
104
AAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEA
105
AQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB
106
AAEAAQABAAEAAQABAAEAAQABAAEAAf//x8ABAP//ubAABP//x8ABCP//x8ABDEVEVABFU1QARVdU
107
AEVQVAAEslgAAAAAAQWk7AEAAAACB4YfggAAAAMJZ1MDAAAABAtIhoQAAAAFDSsLhQAAAAYPDD8G
108
AAAABxDtcocAAAAIEs6mCAAAAAkVn8qJAAAACheA/goAAAALGWIxiwAAAAwdJeoMAAAADSHa5Q0A
109
AAAOJZ6djgAAAA8nf9EPAAAAECpQ9ZAAAAARLDIpEQAAABIuE1ySAAAAEzDnJBMAAAAUM7hIlAAA
110
ABU2jBAVAAAAFkO3G5YAAAAXAAAAAQAAAAE=
111
"""
112

113 84
TZICAL_EST5EDT = """
114
BEGIN:VTIMEZONE
115
TZID:US-Eastern
116
LAST-MODIFIED:19870101T000000Z
117
TZURL:http://zones.stds_r_us.net/tz/US-Eastern
118
BEGIN:STANDARD
119
DTSTART:19671029T020000
120
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
121
TZOFFSETFROM:-0400
122
TZOFFSETTO:-0500
123
TZNAME:EST
124
END:STANDARD
125
BEGIN:DAYLIGHT
126
DTSTART:19870405T020000
127
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
128
TZOFFSETFROM:-0500
129
TZOFFSETTO:-0400
130
TZNAME:EDT
131
END:DAYLIGHT
132
END:VTIMEZONE
133
"""
134

135 84
TZICAL_PST8PDT = """
136
BEGIN:VTIMEZONE
137
TZID:US-Pacific
138
LAST-MODIFIED:19870101T000000Z
139
BEGIN:STANDARD
140
DTSTART:19671029T020000
141
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
142
TZOFFSETFROM:-0700
143
TZOFFSETTO:-0800
144
TZNAME:PST
145
END:STANDARD
146
BEGIN:DAYLIGHT
147
DTSTART:19870405T020000
148
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
149
TZOFFSETFROM:-0800
150
TZOFFSETTO:-0700
151
TZNAME:PDT
152
END:DAYLIGHT
153
END:VTIMEZONE
154
"""
155

156 84
EST_TUPLE = ('EST', timedelta(hours=-5), timedelta(hours=0))
157 84
EDT_TUPLE = ('EDT', timedelta(hours=-4), timedelta(hours=1))
158

159 84
SUPPORTS_SUB_MINUTE_OFFSETS = sys.version_info >= (3, 6)
160

161

162
###
163
# Helper functions
164 84
def get_timezone_tuple(dt):
165
    """Retrieve a (tzname, utcoffset, dst) tuple for a given DST"""
166 84
    return dt.tzname(), dt.utcoffset(), dt.dst()
167

168

169
###
170
# Mix-ins
171 84
class context_passthrough(object):
172 84
    def __init__(*args, **kwargs):
173 84
        pass
174

175 84
    def __enter__(*args, **kwargs):
176 84
        pass
177

178 84
    def __exit__(*args, **kwargs):
179 84
        pass
180

181

182 84
class TzFoldMixin(object):
183
    """ Mix-in class for testing ambiguous times """
184 84
    def gettz(self, tzname):
185 0
        raise NotImplementedError
186

187 84
    def _get_tzname(self, tzname):
188 84
        return tzname
189

190 84
    def _gettz_context(self, tzname):
191 84
        return context_passthrough()
192

193 84
    def testFoldPositiveUTCOffset(self):
194
        # Test that we can resolve ambiguous times
195 84
        tzname = self._get_tzname('Australia/Sydney')
196

197 84
        with self._gettz_context(tzname):
198 84
            SYD = self.gettz(tzname)
199

200 84
            t0_u = datetime(2012, 3, 31, 15, 30, tzinfo=tz.UTC)  # AEST
201 84
            t1_u = datetime(2012, 3, 31, 16, 30, tzinfo=tz.UTC)  # AEDT
202

203 84
            t0_syd0 = t0_u.astimezone(SYD)
204 84
            t1_syd1 = t1_u.astimezone(SYD)
205

206 84
            self.assertEqual(t0_syd0.replace(tzinfo=None),
207
                             datetime(2012, 4, 1, 2, 30))
208

209 84
            self.assertEqual(t1_syd1.replace(tzinfo=None),
210
                             datetime(2012, 4, 1, 2, 30))
211

212 84
            self.assertEqual(t0_syd0.utcoffset(), timedelta(hours=11))
213 84
            self.assertEqual(t1_syd1.utcoffset(), timedelta(hours=10))
214

215 84
    def testGapPositiveUTCOffset(self):
216
        # Test that we don't have a problem around gaps.
217 84
        tzname = self._get_tzname('Australia/Sydney')
218

219 84
        with self._gettz_context(tzname):
220 84
            SYD = self.gettz(tzname)
221

222 84
            t0_u = datetime(2012, 10, 6, 15, 30, tzinfo=tz.UTC)  # AEST
223 84
            t1_u = datetime(2012, 10, 6, 16, 30, tzinfo=tz.UTC)  # AEDT
224

225 84
            t0 = t0_u.astimezone(SYD)
226 84
            t1 = t1_u.astimezone(SYD)
227

228 84
            self.assertEqual(t0.replace(tzinfo=None),
229
                             datetime(2012, 10, 7, 1, 30))
230

231 84
            self.assertEqual(t1.replace(tzinfo=None),
232
                             datetime(2012, 10, 7, 3, 30))
233

234 84
            self.assertEqual(t0.utcoffset(), timedelta(hours=10))
235 84
            self.assertEqual(t1.utcoffset(), timedelta(hours=11))
236

237 84
    def testFoldNegativeUTCOffset(self):
238
            # Test that we can resolve ambiguous times
239 84
            tzname = self._get_tzname('America/Toronto')
240

241 84
            with self._gettz_context(tzname):
242 84
                TOR = self.gettz(tzname)
243

244 84
                t0_u = datetime(2011, 11, 6, 5, 30, tzinfo=tz.UTC)
245 84
                t1_u = datetime(2011, 11, 6, 6, 30, tzinfo=tz.UTC)
246

247 84
                t0_tor = t0_u.astimezone(TOR)
248 84
                t1_tor = t1_u.astimezone(TOR)
249

250 84
                self.assertEqual(t0_tor.replace(tzinfo=None),
251
                                 datetime(2011, 11, 6, 1, 30))
252

253 84
                self.assertEqual(t1_tor.replace(tzinfo=None),
254
                                 datetime(2011, 11, 6, 1, 30))
255

256 84
                self.assertNotEqual(t0_tor.tzname(), t1_tor.tzname())
257 84
                self.assertEqual(t0_tor.utcoffset(), timedelta(hours=-4.0))
258 84
                self.assertEqual(t1_tor.utcoffset(), timedelta(hours=-5.0))
259

260 84
    def testGapNegativeUTCOffset(self):
261
        # Test that we don't have a problem around gaps.
262 84
        tzname = self._get_tzname('America/Toronto')
263

264 84
        with self._gettz_context(tzname):
265 84
            TOR = self.gettz(tzname)
266

267 84
            t0_u = datetime(2011, 3, 13, 6, 30, tzinfo=tz.UTC)
268 84
            t1_u = datetime(2011, 3, 13, 7, 30, tzinfo=tz.UTC)
269

270 84
            t0 = t0_u.astimezone(TOR)
271 84
            t1 = t1_u.astimezone(TOR)
272

273 84
            self.assertEqual(t0.replace(tzinfo=None),
274
                             datetime(2011, 3, 13, 1, 30))
275

276 84
            self.assertEqual(t1.replace(tzinfo=None),
277
                             datetime(2011, 3, 13, 3, 30))
278

279 84
            self.assertNotEqual(t0, t1)
280 84
            self.assertEqual(t0.utcoffset(), timedelta(hours=-5.0))
281 84
            self.assertEqual(t1.utcoffset(), timedelta(hours=-4.0))
282

283 84
    def testFoldLondon(self):
284 84
        tzname = self._get_tzname('Europe/London')
285

286 84
        with self._gettz_context(tzname):
287 84
            LON = self.gettz(tzname)
288 84
            UTC = tz.UTC
289

290 84
            t0_u = datetime(2013, 10, 27, 0, 30, tzinfo=UTC)   # BST
291 84
            t1_u = datetime(2013, 10, 27, 1, 30, tzinfo=UTC)   # GMT
292

293 84
            t0 = t0_u.astimezone(LON)
294 84
            t1 = t1_u.astimezone(LON)
295

296 84
            self.assertEqual(t0.replace(tzinfo=None),
297
                             datetime(2013, 10, 27, 1, 30))
298

299 84
            self.assertEqual(t1.replace(tzinfo=None),
300
                             datetime(2013, 10, 27, 1, 30))
301

302 84
            self.assertEqual(t0.utcoffset(), timedelta(hours=1))
303 84
            self.assertEqual(t1.utcoffset(), timedelta(hours=0))
304

305 84
    def testFoldIndependence(self):
306 84
        tzname = self._get_tzname('America/New_York')
307

308 84
        with self._gettz_context(tzname):
309 84
            NYC = self.gettz(tzname)
310 84
            UTC = tz.UTC
311 84
            hour = timedelta(hours=1)
312

313
            # Firmly 2015-11-01 0:30 EDT-4
314 84
            pre_dst = datetime(2015, 11, 1, 0, 30, tzinfo=NYC)
315

316
            # Ambiguous between 2015-11-01 1:30 EDT-4 and 2015-11-01 1:30 EST-5
317 84
            in_dst = pre_dst + hour
318 84
            in_dst_tzname_0 = in_dst.tzname()     # Stash the tzname - EDT
319

320
            # Doing the arithmetic in UTC creates a date that is unambiguously
321
            # 2015-11-01 1:30 EDT-5
322 84
            in_dst_via_utc = (pre_dst.astimezone(UTC) + 2*hour).astimezone(NYC)
323

324
            # Make sure the dates are actually ambiguous
325 84
            self.assertEqual(in_dst, in_dst_via_utc)
326

327
            # Make sure we got the right folding behavior
328 84
            self.assertNotEqual(in_dst_via_utc.tzname(), in_dst_tzname_0)
329

330
            # Now check to make sure in_dst's tzname hasn't changed
331 84
            self.assertEqual(in_dst_tzname_0, in_dst.tzname())
332

333 84
    def testInZoneFoldEquality(self):
334
        # Two datetimes in the same zone are considered to be equal if their
335
        # wall times are equal, even if they have different absolute times.
336

337 84
        tzname = self._get_tzname('America/New_York')
338

339 84
        with self._gettz_context(tzname):
340 84
            NYC = self.gettz(tzname)
341 84
            UTC = tz.UTC
342

343 84
            dt0 = datetime(2011, 11, 6, 1, 30, tzinfo=NYC)
344 84
            dt1 = tz.enfold(dt0, fold=1)
345

346
            # Make sure these actually represent different times
347 84
            self.assertNotEqual(dt0.astimezone(UTC), dt1.astimezone(UTC))
348

349
            # Test that they compare equal
350 84
            self.assertEqual(dt0, dt1)
351

352 84
    def _test_ambiguous_time(self, dt, tzid, ambiguous):
353
        # This is a test to check that the individual is_ambiguous values
354
        # on the _tzinfo subclasses work.
355 84
        tzname = self._get_tzname(tzid)
356

357 84
        with self._gettz_context(tzname):
358 84
            tzi = self.gettz(tzname)
359

360 84
            self.assertEqual(tz.datetime_ambiguous(dt, tz=tzi), ambiguous)
361

362 84
    def testAmbiguousNegativeUTCOffset(self):
363 84
        self._test_ambiguous_time(datetime(2015, 11, 1, 1, 30),
364
                                  'America/New_York', True)
365

366 84
    def testAmbiguousPositiveUTCOffset(self):
367 84
        self._test_ambiguous_time(datetime(2012, 4, 1, 2, 30),
368
                                  'Australia/Sydney', True)
369

370 84
    def testUnambiguousNegativeUTCOffset(self):
371 84
        self._test_ambiguous_time(datetime(2015, 11, 1, 2, 30),
372
                                  'America/New_York', False)
373

374 84
    def testUnambiguousPositiveUTCOffset(self):
375 84
        self._test_ambiguous_time(datetime(2012, 4, 1, 3, 30),
376
                                  'Australia/Sydney', False)
377

378 84
    def testUnambiguousGapNegativeUTCOffset(self):
379
        # Imaginary time
380 84
        self._test_ambiguous_time(datetime(2011, 3, 13, 2, 30),
381
                                  'America/New_York', False)
382

383 84
    def testUnambiguousGapPositiveUTCOffset(self):
384
        # Imaginary time
385 84
        self._test_ambiguous_time(datetime(2012, 10, 7, 2, 30),
386
                                  'Australia/Sydney', False)
387

388 84
    def _test_imaginary_time(self, dt, tzid, exists):
389 84
        tzname = self._get_tzname(tzid)
390 84
        with self._gettz_context(tzname):
391 84
            tzi = self.gettz(tzname)
392

393 84
            self.assertEqual(tz.datetime_exists(dt, tz=tzi), exists)
394

395 84
    def testImaginaryNegativeUTCOffset(self):
396 84
        self._test_imaginary_time(datetime(2011, 3, 13, 2, 30),
397
                                  'America/New_York', False)
398

399 84
    def testNotImaginaryNegativeUTCOffset(self):
400 84
        self._test_imaginary_time(datetime(2011, 3, 13, 1, 30),
401
                                  'America/New_York', True)
402

403 84
    def testImaginaryPositiveUTCOffset(self):
404 84
        self._test_imaginary_time(datetime(2012, 10, 7, 2, 30),
405
                                  'Australia/Sydney', False)
406

407 84
    def testNotImaginaryPositiveUTCOffset(self):
408 84
        self._test_imaginary_time(datetime(2012, 10, 7, 1, 30),
409
                                  'Australia/Sydney', True)
410

411 84
    def testNotImaginaryFoldNegativeUTCOffset(self):
412 84
        self._test_imaginary_time(datetime(2015, 11, 1, 1, 30),
413
                                  'America/New_York', True)
414

415 84
    def testNotImaginaryFoldPositiveUTCOffset(self):
416 84
        self._test_imaginary_time(datetime(2012, 4, 1, 3, 30),
417
                                  'Australia/Sydney', True)
418

419 84
    @unittest.skip("Known failure in Python 3.6.")
420 27
    def testEqualAmbiguousComparison(self):
421 0
        tzname = self._get_tzname('Australia/Sydney')
422

423 0
        with self._gettz_context(tzname):
424 0
            SYD0 = self.gettz(tzname)
425 0
            SYD1 = self.gettz(tzname)
426

427 0
            t0_u = datetime(2012, 3, 31, 14, 30, tzinfo=tz.UTC)  # AEST
428

429 0
            t0_syd0 = t0_u.astimezone(SYD0)
430 0
            t0_syd1 = t0_u.astimezone(SYD1)
431

432
            # This is considered an "inter-zone comparison" because it's an
433
            # ambiguous datetime.
434 0
            self.assertEqual(t0_syd0, t0_syd1)
435

436

437 84
class TzWinFoldMixin(object):
438 84
    def get_args(self, tzname):
439 9
        return (tzname, )
440

441 84
    class context(object):
442 84
        def __init__(*args, **kwargs):
443 9
            pass
444

445 84
        def __enter__(*args, **kwargs):
446 9
            pass
447

448 84
        def __exit__(*args, **kwargs):
449 9
            pass
450

451 84
    def get_utc_transitions(self, tzi, year, gap):
452 9
        dston, dstoff = tzi.transitions(year)
453 9
        if gap:
454 9
            t_n = dston - timedelta(minutes=30)
455

456 9
            t0_u = t_n.replace(tzinfo=tzi).astimezone(tz.UTC)
457 9
            t1_u = t0_u + timedelta(hours=1)
458
        else:
459
            # Get 1 hour before the first ambiguous date
460 9
            t_n = dstoff - timedelta(minutes=30)
461

462 9
            t0_u = t_n.replace(tzinfo=tzi).astimezone(tz.UTC)
463 9
            t_n += timedelta(hours=1)                   # Naive ambiguous date
464 9
            t0_u = t0_u + timedelta(hours=1)            # First ambiguous date
465 9
            t1_u = t0_u + timedelta(hours=1)            # Second ambiguous date
466

467 9
        return t_n, t0_u, t1_u
468

469 84
    def testFoldPositiveUTCOffset(self):
470
        # Test that we can resolve ambiguous times
471 9
        tzname = 'AUS Eastern Standard Time'
472 9
        args = self.get_args(tzname)
473

474 9
        with self.context(tzname):
475
            # Calling fromutc() alters the tzfile object
476 9
            SYD = self.tzclass(*args)
477

478
            # Get the transition time in UTC from the object, because
479
            # Windows doesn't store historical info
480 9
            t_n, t0_u, t1_u = self.get_utc_transitions(SYD, 2012, False)
481

482
            # Using fresh tzfiles
483 9
            t0_syd = t0_u.astimezone(SYD)
484 9
            t1_syd = t1_u.astimezone(SYD)
485

486 9
            self.assertEqual(t0_syd.replace(tzinfo=None), t_n)
487

488 9
            self.assertEqual(t1_syd.replace(tzinfo=None), t_n)
489

490 9
            self.assertEqual(t0_syd.utcoffset(), timedelta(hours=11))
491 9
            self.assertEqual(t1_syd.utcoffset(), timedelta(hours=10))
492 9
            self.assertNotEqual(t0_syd.tzname(), t1_syd.tzname())
493

494 84
    def testGapPositiveUTCOffset(self):
495
        # Test that we don't have a problem around gaps.
496 9
        tzname = 'AUS Eastern Standard Time'
497 9
        args = self.get_args(tzname)
498

499 9
        with self.context(tzname):
500 9
            SYD = self.tzclass(*args)
501

502 9
            t_n, t0_u, t1_u = self.get_utc_transitions(SYD, 2012, True)
503

504 9
            t0 = t0_u.astimezone(SYD)
505 9
            t1 = t1_u.astimezone(SYD)
506

507 9
            self.assertEqual(t0.replace(tzinfo=None), t_n)
508

509 9
            self.assertEqual(t1.replace(tzinfo=None), t_n + timedelta(hours=2))
510

511 9
            self.assertEqual(t0.utcoffset(), timedelta(hours=10))
512 9
            self.assertEqual(t1.utcoffset(), timedelta(hours=11))
513

514 84
    def testFoldNegativeUTCOffset(self):
515
        # Test that we can resolve ambiguous times
516 9
        tzname = 'Eastern Standard Time'
517 9
        args = self.get_args(tzname)
518

519 9
        with self.context(tzname):
520 9
            TOR = self.tzclass(*args)
521

522 9
            t_n, t0_u, t1_u = self.get_utc_transitions(TOR, 2011, False)
523

524 9
            t0_tor = t0_u.astimezone(TOR)
525 9
            t1_tor = t1_u.astimezone(TOR)
526

527 9
            self.assertEqual(t0_tor.replace(tzinfo=None), t_n)
528 9
            self.assertEqual(t1_tor.replace(tzinfo=None), t_n)
529

530 9
            self.assertNotEqual(t0_tor.tzname(), t1_tor.tzname())
531 9
            self.assertEqual(t0_tor.utcoffset(), timedelta(hours=-4.0))
532 9
            self.assertEqual(t1_tor.utcoffset(), timedelta(hours=-5.0))
533

534 84
    def testGapNegativeUTCOffset(self):
535
        # Test that we don't have a problem around gaps.
536 9
        tzname = 'Eastern Standard Time'
537 9
        args = self.get_args(tzname)
538

539 9
        with self.context(tzname):
540 9
            TOR = self.tzclass(*args)
541

542 9
            t_n, t0_u, t1_u = self.get_utc_transitions(TOR, 2011, True)
543

544 9
            t0 = t0_u.astimezone(TOR)
545 9
            t1 = t1_u.astimezone(TOR)
546

547 9
            self.assertEqual(t0.replace(tzinfo=None),
548
                             t_n)
549

550 9
            self.assertEqual(t1.replace(tzinfo=None),
551
                             t_n + timedelta(hours=2))
552

553 9
            self.assertNotEqual(t0.tzname(), t1.tzname())
554 9
            self.assertEqual(t0.utcoffset(), timedelta(hours=-5.0))
555 9
            self.assertEqual(t1.utcoffset(), timedelta(hours=-4.0))
556

557 84
    def testFoldIndependence(self):
558 9
        tzname = 'Eastern Standard Time'
559 9
        args = self.get_args(tzname)
560

561 9
        with self.context(tzname):
562 9
            NYC = self.tzclass(*args)
563 9
            UTC = tz.UTC
564 9
            hour = timedelta(hours=1)
565

566
            # Firmly 2015-11-01 0:30 EDT-4
567 9
            t_n, t0_u, t1_u = self.get_utc_transitions(NYC, 2015, False)
568

569 9
            pre_dst = (t_n - hour).replace(tzinfo=NYC)
570

571
            # Currently, there's no way around the fact that this resolves to an
572
            # ambiguous date, which defaults to EST. I'm not hard-coding in the
573
            # answer, though, because the preferred behavior would be that this
574
            # results in a time on the EDT side.
575

576
            # Ambiguous between 2015-11-01 1:30 EDT-4 and 2015-11-01 1:30 EST-5
577 9
            in_dst = pre_dst + hour
578 9
            in_dst_tzname_0 = in_dst.tzname()     # Stash the tzname - EDT
579

580
            # Doing the arithmetic in UTC creates a date that is unambiguously
581
            # 2015-11-01 1:30 EDT-5
582 9
            in_dst_via_utc = (pre_dst.astimezone(UTC) + 2*hour).astimezone(NYC)
583

584
            # Make sure we got the right folding behavior
585 9
            self.assertNotEqual(in_dst_via_utc.tzname(), in_dst_tzname_0)
586

587
            # Now check to make sure in_dst's tzname hasn't changed
588 9
            self.assertEqual(in_dst_tzname_0, in_dst.tzname())
589

590 84
    def testInZoneFoldEquality(self):
591
        # Two datetimes in the same zone are considered to be equal if their
592
        # wall times are equal, even if they have different absolute times.
593 9
        tzname = 'Eastern Standard Time'
594 9
        args = self.get_args(tzname)
595

596 9
        with self.context(tzname):
597 9
            NYC = self.tzclass(*args)
598 9
            UTC = tz.UTC
599

600 9
            t_n, t0_u, t1_u = self.get_utc_transitions(NYC, 2011, False)
601

602 9
            dt0 = t_n.replace(tzinfo=NYC)
603 9
            dt1 = tz.enfold(dt0, fold=1)
604

605
            # Make sure these actually represent different times
606 9
            self.assertNotEqual(dt0.astimezone(UTC), dt1.astimezone(UTC))
607

608
            # Test that they compare equal
609 9
            self.assertEqual(dt0, dt1)
610

611
###
612
# Test Cases
613 84
class TzUTCTest(unittest.TestCase):
614 84
    def testSingleton(self):
615 84
        UTC_0 = tz.tzutc()
616 84
        UTC_1 = tz.tzutc()
617

618 84
        self.assertIs(UTC_0, UTC_1)
619

620 84
    def testOffset(self):
621 84
        ct = datetime(2009, 4, 1, 12, 11, 13, tzinfo=tz.tzutc())
622

623 84
        self.assertEqual(ct.utcoffset(), timedelta(seconds=0))
624

625 84
    def testDst(self):
626 84
        ct = datetime(2009, 4, 1, 12, 11, 13, tzinfo=tz.tzutc())
627

628 84
        self.assertEqual(ct.dst(), timedelta(seconds=0))
629

630 84
    def testTzName(self):
631 84
        ct = datetime(2009, 4, 1, 12, 11, 13, tzinfo=tz.tzutc())
632 84
        self.assertEqual(ct.tzname(), 'UTC')
633

634 84
    def testEquality(self):
635 84
        UTC0 = tz.tzutc()
636 84
        UTC1 = tz.tzutc()
637

638 84
        self.assertEqual(UTC0, UTC1)
639

640 84
    def testInequality(self):
641 84
        UTC = tz.tzutc()
642 84
        UTCp4 = tz.tzoffset('UTC+4', 14400)
643

644 84
        self.assertNotEqual(UTC, UTCp4)
645

646 84
    def testInequalityInteger(self):
647 84
        self.assertFalse(tz.tzutc() == 7)
648 84
        self.assertNotEqual(tz.tzutc(), 7)
649

650 84
    def testInequalityUnsupported(self):
651 84
        self.assertEqual(tz.tzutc(), ComparesEqual)
652

653 84
    def testRepr(self):
654 84
        UTC = tz.tzutc()
655 84
        self.assertEqual(repr(UTC), 'tzutc()')
656

657 84
    def testTimeOnlyUTC(self):
658
        # https://github.com/dateutil/dateutil/issues/132
659
        # tzutc doesn't care
660 84
        tz_utc = tz.tzutc()
661 84
        self.assertEqual(dt_time(13, 20, tzinfo=tz_utc).utcoffset(),
662
                         timedelta(0))
663

664 84
    def testAmbiguity(self):
665
        # Pick an arbitrary datetime, this should always return False.
666 84
        dt = datetime(2011, 9, 1, 2, 30, tzinfo=tz.tzutc())
667

668 84
        self.assertFalse(tz.datetime_ambiguous(dt))
669

670

671 84
@pytest.mark.tzoffset
672 84
class TzOffsetTest(unittest.TestCase):
673 84
    def testTimedeltaOffset(self):
674 84
        est = tz.tzoffset('EST', timedelta(hours=-5))
675 84
        est_s = tz.tzoffset('EST', -18000)
676

677 84
        self.assertEqual(est, est_s)
678

679 84
    def testTzNameNone(self):
680 84
        gmt5 = tz.tzoffset(None, -18000)       # -5:00
681 84
        self.assertIs(datetime(2003, 10, 26, 0, 0, tzinfo=gmt5).tzname(),
682
                      None)
683

684 84
    def testTimeOnlyOffset(self):
685
        # tzoffset doesn't care
686 84
        tz_offset = tz.tzoffset('+3', 3600)
687 84
        self.assertEqual(dt_time(13, 20, tzinfo=tz_offset).utcoffset(),
688
                         timedelta(seconds=3600))
689

690 84
    def testTzOffsetRepr(self):
691 84
        tname = 'EST'
692 84
        tzo = tz.tzoffset(tname, -5 * 3600)
693 84
        self.assertEqual(repr(tzo), "tzoffset(" + repr(tname) + ", -18000)")
694

695 84
    def testEquality(self):
696 84
        utc = tz.tzoffset('UTC', 0)
697 84
        gmt = tz.tzoffset('GMT', 0)
698

699 84
        self.assertEqual(utc, gmt)
700

701 84
    def testUTCEquality(self):
702 84
        utc = tz.UTC
703 84
        o_utc = tz.tzoffset('UTC', 0)
704

705 84
        self.assertEqual(utc, o_utc)
706 84
        self.assertEqual(o_utc, utc)
707

708 84
    def testInequalityInvalid(self):
709 84
        tzo = tz.tzoffset('-3', -3 * 3600)
710 84
        self.assertFalse(tzo == -3)
711 84
        self.assertNotEqual(tzo, -3)
712

713 84
    def testInequalityUnsupported(self):
714 84
        tzo = tz.tzoffset('-5', -5 * 3600)
715

716 84
        self.assertTrue(tzo == ComparesEqual)
717 84
        self.assertFalse(tzo != ComparesEqual)
718 84
        self.assertEqual(tzo, ComparesEqual)
719

720 84
    def testAmbiguity(self):
721
        # Pick an arbitrary datetime, this should always return False.
722 84
        dt = datetime(2011, 9, 1, 2, 30, tzinfo=tz.tzoffset("EST", -5 * 3600))
723

724 84
        self.assertFalse(tz.datetime_ambiguous(dt))
725

726 84
    def testTzOffsetInstance(self):
727 84
        tz1 = tz.tzoffset.instance('EST', timedelta(hours=-5))
728 84
        tz2 = tz.tzoffset.instance('EST', timedelta(hours=-5))
729

730 84
        assert tz1 is not tz2
731

732 84
    def testTzOffsetSingletonDifferent(self):
733 84
        tz1 = tz.tzoffset('EST', timedelta(hours=-5))
734 84
        tz2 = tz.tzoffset('EST', -18000)
735

736 84
        assert tz1 is tz2
737

738

739 84
@pytest.mark.smoke
740 84
@pytest.mark.tzoffset
741 27
def test_tzoffset_weakref():
742 84
    UTC1 = tz.tzoffset('UTC', 0)
743 84
    UTC_ref = weakref.ref(tz.tzoffset('UTC', 0))
744 84
    UTC1 is UTC_ref()
745 84
    del UTC1
746 84
    gc.collect()
747

748 84
    assert UTC_ref() is not None    # Should be in the strong cache
749 84
    assert UTC_ref() is tz.tzoffset('UTC', 0)
750

751
    # Fill the strong cache with other items
752 84
    for offset in range(5,15):
753 84
        tz.tzoffset('RandomZone', offset)
754

755 84
    gc.collect()
756 84
    assert UTC_ref() is  None
757 84
    assert UTC_ref() is not tz.tzoffset('UTC', 0)
758

759

760 84
@pytest.mark.tzoffset
761 84
@pytest.mark.parametrize('args', [
762
    ('UTC', 0),
763
    ('EST', -18000),
764
    ('EST', timedelta(hours=-5)),
765
    (None, timedelta(hours=3)),
766
])
767 27
def test_tzoffset_singleton(args):
768 84
    tz1 = tz.tzoffset(*args)
769 84
    tz2 = tz.tzoffset(*args)
770

771 84
    assert tz1 is tz2
772

773

774 84
@pytest.mark.tzoffset
775 84
@pytest.mark.skipif(not SUPPORTS_SUB_MINUTE_OFFSETS,
776
                    reason='Sub-minute offsets not supported')
777 27
def test_tzoffset_sub_minute():
778 52
    delta = timedelta(hours=12, seconds=30)
779 52
    test_datetime = datetime(2000, 1, 1, tzinfo=tz.tzoffset(None, delta))
780 52
    assert test_datetime.utcoffset() == delta
781

782

783 84
@pytest.mark.tzoffset
784 84
@pytest.mark.skipif(SUPPORTS_SUB_MINUTE_OFFSETS,
785
                    reason='Sub-minute offsets supported')
786 27
def test_tzoffset_sub_minute_rounding():
787 32
    delta = timedelta(hours=12, seconds=30)
788 32
    test_date = datetime(2000, 1, 1, tzinfo=tz.tzoffset(None, delta))
789 32
    assert test_date.utcoffset() == timedelta(hours=12, minutes=1)
790

791

792 84
@pytest.mark.tzlocal
793 84
class TzLocalTest(unittest.TestCase):
794 84
    def testEquality(self):
795 84
        tz1 = tz.tzlocal()
796 84
        tz2 = tz.tzlocal()
797

798
        # Explicitly calling == and != here to ensure the operators work
799 84
        self.assertTrue(tz1 == tz2)
800 84
        self.assertFalse(tz1 != tz2)
801

802 84
    def testInequalityFixedOffset(self):
803 84
        tzl = tz.tzlocal()
804 84
        tzos = tz.tzoffset('LST', tzl._std_offset.total_seconds())
805 84
        tzod = tz.tzoffset('LDT', tzl._std_offset.total_seconds())
806

807 84
        self.assertFalse(tzl == tzos)
808 84
        self.assertFalse(tzl == tzod)
809 84
        self.assertTrue(tzl != tzos)
810 84
        self.assertTrue(tzl != tzod)
811

812 84
    def testInequalityInvalid(self):
813 84
        tzl = tz.tzlocal()
814

815 84
        self.assertTrue(tzl != 1)
816 84
        self.assertFalse(tzl == 1)
817

818
        # TODO: Use some sort of universal local mocking so that it's clear
819
        # that we're expecting tzlocal to *not* be Pacific/Kiritimati
820 84
        LINT = tz.gettz('Pacific/Kiritimati')
821 84
        self.assertTrue(tzl != LINT)
822 84
        self.assertFalse(tzl == LINT)
823

824 84
    def testInequalityUnsupported(self):
825 84
        tzl = tz.tzlocal()
826

827 84
        self.assertTrue(tzl == ComparesEqual)
828 84
        self.assertFalse(tzl != ComparesEqual)
829

830 84
    def testRepr(self):
831 84
        tzl = tz.tzlocal()
832

833 84
        self.assertEqual(repr(tzl), 'tzlocal()')
834

835

836 84
@pytest.mark.parametrize('args,kwargs', [
837
    (('EST', -18000), {}),
838
    (('EST', timedelta(hours=-5)), {}),
839
    (('EST',), {'offset': -18000}),
840
    (('EST',), {'offset': timedelta(hours=-5)}),
841
    (tuple(), {'name': 'EST', 'offset': -18000})
842
])
843 27
def test_tzoffset_is(args, kwargs):
844 84
    tz_ref = tz.tzoffset('EST', -18000)
845 84
    assert tz.tzoffset(*args, **kwargs) is tz_ref
846

847

848 84
def test_tzoffset_is_not():
849 84
    assert tz.tzoffset('EDT', -14400) is not tz.tzoffset('EST', -18000)
850

851

852 84
@pytest.mark.tzlocal
853 84
@unittest.skipIf(IS_WIN, "requires Unix")
854 84
class TzLocalNixTest(unittest.TestCase, TzFoldMixin):
855
    # This is a set of tests for `tzlocal()` on *nix systems
856

857
    # POSIX string indicating change to summer time on the 2nd Sunday in March
858
    # at 2AM, and ending the 1st Sunday in November at 2AM. (valid >= 2007)
859 84
    TZ_EST = 'EST+5EDT,M3.2.0/2,M11.1.0/2'
860

861
    # POSIX string for AEST/AEDT (valid >= 2008)
862 84
    TZ_AEST = 'AEST-10AEDT,M10.1.0/2,M4.1.0/3'
863

864
    # POSIX string for BST/GMT
865 84
    TZ_LON = 'GMT0BST,M3.5.0,M10.5.0'
866

867
    # POSIX string for UTC
868 84
    UTC = 'UTC'
869

870 84
    def gettz(self, tzname):
871
        # Actual time zone changes are handled by the _gettz_context function
872 75
        return tz.tzlocal()
873

874 84
    def _gettz_context(self, tzname):
875 75
        tzname_map = {'Australia/Sydney': self.TZ_AEST,
876
                      'America/Toronto': self.TZ_EST,
877
                      'America/New_York': self.TZ_EST,
878
                      'Europe/London': self.TZ_LON}
879

880 75
        return TZEnvContext(tzname_map.get(tzname, tzname))
881

882 84
    def _testTzFunc(self, tzval, func, std_val, dst_val):
883
        """
884
        This generates tests about how the behavior of a function ``func``
885
        changes between STD and DST (e.g. utcoffset, tzname, dst).
886

887
        It assume that DST starts the 2nd Sunday in March and ends the 1st
888
        Sunday in November
889
        """
890 75
        with TZEnvContext(tzval):
891 75
            dt1 = datetime(2015, 2, 1, 12, 0, tzinfo=tz.tzlocal())  # STD
892 75
            dt2 = datetime(2015, 5, 1, 12, 0, tzinfo=tz.tzlocal())  # DST
893

894 75
            self.assertEqual(func(dt1), std_val)
895 75
            self.assertEqual(func(dt2), dst_val)
896

897 84
    def _testTzName(self, tzval, std_name, dst_name):
898 75
        func = datetime.tzname
899

900 75
        self._testTzFunc(tzval, func, std_name, dst_name)
901

902 84
    def testTzNameDST(self):
903
        # Test tzname in a zone with DST
904 75
        self._testTzName(self.TZ_EST, 'EST', 'EDT')
905

906 84
    def testTzNameUTC(self):
907
        # Test tzname in a zone without DST
908 75
        self._testTzName(self.UTC, 'UTC', 'UTC')
909

910 84
    def _testOffset(self, tzval, std_off, dst_off):
911 75
        func = datetime.utcoffset
912

913 75
        self._testTzFunc(tzval, func, std_off, dst_off)
914

915 84
    def testOffsetDST(self):
916 75
        self._testOffset(self.TZ_EST, timedelta(hours=-5), timedelta(hours=-4))
917

918 84
    def testOffsetUTC(self):
919 75
        self._testOffset(self.UTC, timedelta(0), timedelta(0))
920

921 84
    def _testDST(self, tzval, dst_dst):
922 75
        func = datetime.dst
923 75
        std_dst = timedelta(0)
924

925 75
        self._testTzFunc(tzval, func, std_dst, dst_dst)
926

927 84
    def testDSTDST(self):
928 75
        self._testDST(self.TZ_EST, timedelta(hours=1))
929

930 84
    def testDSTUTC(self):
931 75
        self._testDST(self.UTC, timedelta(0))
932

933 84
    def testTimeOnlyOffsetLocalUTC(self):
934 75
        with TZEnvContext(self.UTC):
935