• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# -*- coding: utf-8 -*-
2from __future__ import unicode_literals
3from ._common import PicklableMixin
4from ._common import TZEnvContext, TZWinContext
5from ._common import WarningTestMixin
6from ._common import ComparesEqual
7
8from datetime import datetime, timedelta
9from datetime import time as dt_time
10from datetime import tzinfo
11from six import PY2
12from io import BytesIO, StringIO
13import unittest
14
15import sys
16import base64
17import copy
18import gc
19import weakref
20
21from functools import partial
22
23IS_WIN = sys.platform.startswith('win')
24
25import pytest
26
27# dateutil imports
28from dateutil.relativedelta import relativedelta, SU, TH
29from dateutil.parser import parse
30from dateutil import tz as tz
31from dateutil import zoneinfo
32
33try:
34    from dateutil import tzwin
35except ImportError as e:
36    if IS_WIN:
37        raise e
38    else:
39        pass
40
41MISSING_TARBALL = ("This test fails if you don't have the dateutil "
42                   "timezone file installed. Please read the README")
43
44TZFILE_EST5EDT = b"""
45VFppZgAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAADrAAAABAAAABCeph5wn7rrYKCGAHCh
46ms1gomXicKOD6eCkaq5wpTWnYKZTyvCnFYlgqDOs8Kj+peCqE47wqt6H4KvzcPCsvmngrdNS8K6e
47S+CvszTwsH4t4LGcUXCyZ0pgs3wzcLRHLGC1XBVwticOYLc793C4BvBguRvZcLnm0mC7BPXwu8a0
48YLzk1/C9r9DgvsS58L+PsuDApJvwwW+U4MKEffDDT3bgxGRf8MUvWODGTXxwxw864MgtXnDI+Fdg
49yg1AcMrYOWDLiPBw0iP0cNJg++DTdeTw1EDd4NVVxvDWIL/g1zWo8NgAoeDZFYrw2eCD4Nr+p3Db
50wGXg3N6JcN2pgmDevmtw34lkYOCeTXDhaUZg4n4vcONJKGDkXhFw5Vcu4OZHLfDnNxDg6CcP8OkW
518uDqBvHw6vbU4Ovm0/Ds1rbg7ca18O6/02Dvr9Jw8J+1YPGPtHDyf5dg82+WcPRfeWD1T3hw9j9b
52YPcvWnD4KHfg+Q88cPoIWeD6+Fjw++g74PzYOvD9yB3g/rgc8P+n/+AAl/7wAYfh4AJ34PADcP5g
53BGD9cAVQ4GAGQN9wBzDCYAeNGXAJEKRgCa2U8ArwhmAL4IVwDNmi4A3AZ3AOuYTgD6mD8BCZZuAR
54iWXwEnlI4BNpR/AUWSrgFUkp8BY5DOAXKQvwGCIpYBkI7fAaAgtgGvIKcBvh7WAc0exwHcHPYB6x
55znAfobFgIHYA8CGBk2AiVeLwI2qv4CQ1xPAlSpHgJhWm8Ccqc+An/sNwKQpV4CnepXAq6jfgK76H
56cCzTVGAtnmlwLrM2YC9+S3AwkxhgMWdn8DJy+mAzR0nwNFLcYDUnK/A2Mr5gNwcN8Dgb2uA45u/w
57Ofu84DrG0fA7257gPK/ucD27gOA+j9BwP5ti4EBvsnBBhH9gQk+UcENkYWBEL3ZwRURDYEYPWHBH
58JCVgR/h08EkEB2BJ2FbwSuPpYEu4OPBMzQXgTZga8E6s5+BPd/zwUIzJ4FFhGXBSbKvgU0D7cFRM
59jeBVIN1wVixv4FcAv3BYFYxgWOChcFn1bmBawINwW9VQYFypn/BdtTJgXomB8F+VFGBgaWPwYX4w
604GJJRfBjXhLgZCkn8GU99OBmEkRwZx3W4GfyJnBo/bjgadIIcGrdmuBrsepwbMa3YG2RzHBupplg
61b3GucHCGe2BxWsrwcmZdYHM6rPB0Rj9gdRqO8HYvW+B2+nDweA894HjaUvB57x/gero08HvPAeB8
62o1Fwfa7j4H6DM3B/jsXgAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB
63AAEAAQABAgMBAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB
64AAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEA
65AQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB
66AAEAAQABAAEAAQABAAEAAQABAAEAAf//x8ABAP//ubAABP//x8ABCP//x8ABDEVEVABFU1QARVdU
67AEVQVAAAAAABAAAAAQ==
68"""
69
70EUROPE_HELSINKI = b"""
71VFppZgAAAAAAAAAAAAAAAAAAAAAAAAAFAAAABQAAAAAAAAB1AAAABQAAAA2kc28Yy85RYMy/hdAV
72I+uQFhPckBcDzZAX876QGOOvkBnToJAaw5GQG7y9EBysrhAdnJ8QHoyQEB98gRAgbHIQIVxjECJM
73VBAjPEUQJCw2ECUcJxAmDBgQJwVDkCf1NJAo5SWQKdUWkCrFB5ArtPiQLKTpkC2U2pAuhMuQL3S8
74kDBkrZAxXdkQMnK0EDM9uxA0UpYQNR2dEDYyeBA2/X8QOBuUkDjdYRA5+3aQOr1DEDvbWJA8pl+Q
75Pbs6kD6GQZA/mxyQQGYjkEGEORBCRgWQQ2QbEEQl55BFQ/0QRgXJkEcj3xBH7uYQSQPBEEnOyBBK
7646MQS66qEEzMv5BNjowQTqyhkE9ubhBQjIOQUVeKkFJsZZBTN2yQVExHkFUXTpBWLCmQVvcwkFgV
77RhBY1xKQWfUoEFq29JBb1QoQXKAREF207BBef/MQX5TOEGBf1RBhfeqQYj+3EGNdzJBkH5kQZT2u
78kGYItZBnHZCQZ+iXkGj9cpBpyHmQat1UkGuoW5BsxnEQbYg9kG6mUxBvaB+QcIY1EHFRPBByZhcQ
79czEeEHRF+RB1EQAQdi8VkHbw4hB4DveQeNDEEHnu2ZB6sKYQe867kHyZwpB9rp2QfnmkkH+Of5AC
80AQIDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQD
81BAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAME
82AwQAABdoAAAAACowAQQAABwgAAkAACowAQQAABwgAAlITVQARUVTVABFRVQAAAAAAQEAAAABAQ==
83"""
84
85NEW_YORK = b"""
86VFppZgAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAABcAAADrAAAABAAAABCeph5wn7rrYKCGAHCh
87ms1gomXicKOD6eCkaq5wpTWnYKZTyvCnFYlgqDOs8Kj+peCqE47wqt6H4KvzcPCsvmngrdNS8K6e
88S+CvszTwsH4t4LGcUXCyZ0pgs3wzcLRHLGC1XBVwticOYLc793C4BvBguRvZcLnm0mC7BPXwu8a0
89YLzk1/C9r9DgvsS58L+PsuDApJvwwW+U4MKEffDDT3bgxGRf8MUvWODGTXxwxw864MgtXnDI+Fdg
90yg1AcMrYOWDLiPBw0iP0cNJg++DTdeTw1EDd4NVVxvDWIL/g1zWo8NgAoeDZFYrw2eCD4Nr+p3Db
91wGXg3N6JcN2pgmDevmtw34lkYOCeTXDhaUZg4n4vcONJKGDkXhFw5Vcu4OZHLfDnNxDg6CcP8OkW
928uDqBvHw6vbU4Ovm0/Ds1rbg7ca18O6/02Dvr9Jw8J+1YPGPtHDyf5dg82+WcPRfeWD1T3hw9j9b
93YPcvWnD4KHfg+Q88cPoIWeD6+Fjw++g74PzYOvD9yB3g/rgc8P+n/+AAl/7wAYfh4AJ34PADcP5g
94BGD9cAVQ4GEGQN9yBzDCYgeNGXMJEKRjCa2U9ArwhmQL4IV1DNmi5Q3AZ3YOuYTmD6mD9xCZZucR
95iWX4EnlI6BNpR/kUWSrpFUkp+RY5DOoXKQv6GCIpaxkI7fsaAgtsGvIKfBvh7Wwc0ex8HcHPbR6x
96zn0fobFtIHYA/SGBk20iVeL+I2qv7iQ1xP4lSpHuJhWm/ycqc+8n/sOAKQpV8CnepYAq6jfxK76H
97gSzTVHItnmmCLrM2cy9+S4MwkxhzMWdoBDJy+nQzR0oENFLcdTUnLAU2Mr51NwcOBjgb2vY45vAG
98Ofu89jrG0gY72572PK/uhj27gPY+j9CGP5ti9kBvsoZBhH92Qk+UhkNkYXZEL3aHRURDd0XzqQdH
99LV/3R9OLB0kNQfdJs20HSu0j90uciYdM1kB3TXxrh062IndPXE2HUJYEd1E8L4dSdeZ3UxwRh1RV
100yHdU+/OHVjWqd1blEAdYHsb3WMTyB1n+qPdapNQHW96K91yEtgddvmz3XmSYB1+eTvdgTbSHYYdr
101d2ItlodjZ013ZA14h2VHL3dl7VqHZycRd2fNPIdpBvN3aa0eh2rm1XdrljsHbM/x9212HQdur9P3
102b1X/B3CPtfdxNeEHcm+X93MVwwd0T3n3dP7fh3Y4lnd23sGHeBh4d3i+o4d5+Fp3ep6Fh3vYPHd8
103fmeHfbged35eSYd/mAB3AAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB
104AAEAAQABAgMBAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB
105AAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEA
106AQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB
107AAEAAQABAAEAAQABAAEAAQABAAEAAf//x8ABAP//ubAABP//x8ABCP//x8ABDEVEVABFU1QARVdU
108AEVQVAAEslgAAAAAAQWk7AEAAAACB4YfggAAAAMJZ1MDAAAABAtIhoQAAAAFDSsLhQAAAAYPDD8G
109AAAABxDtcocAAAAIEs6mCAAAAAkVn8qJAAAACheA/goAAAALGWIxiwAAAAwdJeoMAAAADSHa5Q0A
110AAAOJZ6djgAAAA8nf9EPAAAAECpQ9ZAAAAARLDIpEQAAABIuE1ySAAAAEzDnJBMAAAAUM7hIlAAA
111ABU2jBAVAAAAFkO3G5YAAAAXAAAAAQAAAAE=
112"""
113
114TZICAL_EST5EDT = """
115BEGIN:VTIMEZONE
116TZID:US-Eastern
117LAST-MODIFIED:19870101T000000Z
118TZURL:http://zones.stds_r_us.net/tz/US-Eastern
119BEGIN:STANDARD
120DTSTART:19671029T020000
121RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
122TZOFFSETFROM:-0400
123TZOFFSETTO:-0500
124TZNAME:EST
125END:STANDARD
126BEGIN:DAYLIGHT
127DTSTART:19870405T020000
128RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
129TZOFFSETFROM:-0500
130TZOFFSETTO:-0400
131TZNAME:EDT
132END:DAYLIGHT
133END:VTIMEZONE
134"""
135
136TZICAL_PST8PDT = """
137BEGIN:VTIMEZONE
138TZID:US-Pacific
139LAST-MODIFIED:19870101T000000Z
140BEGIN:STANDARD
141DTSTART:19671029T020000
142RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
143TZOFFSETFROM:-0700
144TZOFFSETTO:-0800
145TZNAME:PST
146END:STANDARD
147BEGIN:DAYLIGHT
148DTSTART:19870405T020000
149RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
150TZOFFSETFROM:-0800
151TZOFFSETTO:-0700
152TZNAME:PDT
153END:DAYLIGHT
154END:VTIMEZONE
155"""
156
157EST_TUPLE = ('EST', timedelta(hours=-5), timedelta(hours=0))
158EDT_TUPLE = ('EDT', timedelta(hours=-4), timedelta(hours=1))
159
160SUPPORTS_SUB_MINUTE_OFFSETS = sys.version_info >= (3, 6)
161
162
163###
164# Helper functions
165def get_timezone_tuple(dt):
166    """Retrieve a (tzname, utcoffset, dst) tuple for a given DST"""
167    return dt.tzname(), dt.utcoffset(), dt.dst()
168
169
170###
171# Mix-ins
172class context_passthrough(object):
173    def __init__(*args, **kwargs):
174        pass
175
176    def __enter__(*args, **kwargs):
177        pass
178
179    def __exit__(*args, **kwargs):
180        pass
181
182
183class TzFoldMixin(object):
184    """ Mix-in class for testing ambiguous times """
185    def gettz(self, tzname):
186        raise NotImplementedError
187
188    def _get_tzname(self, tzname):
189        return tzname
190
191    def _gettz_context(self, tzname):
192        return context_passthrough()
193
194    def testFoldPositiveUTCOffset(self):
195        # Test that we can resolve ambiguous times
196        tzname = self._get_tzname('Australia/Sydney')
197
198        with self._gettz_context(tzname):
199            SYD = self.gettz(tzname)
200
201            t0_u = datetime(2012, 3, 31, 15, 30, tzinfo=tz.tzutc())  # AEST
202            t1_u = datetime(2012, 3, 31, 16, 30, tzinfo=tz.tzutc())  # AEDT
203
204            t0_syd0 = t0_u.astimezone(SYD)
205            t1_syd1 = t1_u.astimezone(SYD)
206
207            self.assertEqual(t0_syd0.replace(tzinfo=None),
208                             datetime(2012, 4, 1, 2, 30))
209
210            self.assertEqual(t1_syd1.replace(tzinfo=None),
211                             datetime(2012, 4, 1, 2, 30))
212
213            self.assertEqual(t0_syd0.utcoffset(), timedelta(hours=11))
214            self.assertEqual(t1_syd1.utcoffset(), timedelta(hours=10))
215
216    def testGapPositiveUTCOffset(self):
217        # Test that we don't have a problem around gaps.
218        tzname = self._get_tzname('Australia/Sydney')
219
220        with self._gettz_context(tzname):
221            SYD = self.gettz(tzname)
222
223            t0_u = datetime(2012, 10, 6, 15, 30, tzinfo=tz.tzutc())  # AEST
224            t1_u = datetime(2012, 10, 6, 16, 30, tzinfo=tz.tzutc())  # AEDT
225
226            t0 = t0_u.astimezone(SYD)
227            t1 = t1_u.astimezone(SYD)
228
229            self.assertEqual(t0.replace(tzinfo=None),
230                             datetime(2012, 10, 7, 1, 30))
231
232            self.assertEqual(t1.replace(tzinfo=None),
233                             datetime(2012, 10, 7, 3, 30))
234
235            self.assertEqual(t0.utcoffset(), timedelta(hours=10))
236            self.assertEqual(t1.utcoffset(), timedelta(hours=11))
237
238    def testFoldNegativeUTCOffset(self):
239            # Test that we can resolve ambiguous times
240            tzname = self._get_tzname('America/Toronto')
241
242            with self._gettz_context(tzname):
243                TOR = self.gettz(tzname)
244
245                t0_u = datetime(2011, 11, 6, 5, 30, tzinfo=tz.tzutc())
246                t1_u = datetime(2011, 11, 6, 6, 30, tzinfo=tz.tzutc())
247
248                t0_tor = t0_u.astimezone(TOR)
249                t1_tor = t1_u.astimezone(TOR)
250
251                self.assertEqual(t0_tor.replace(tzinfo=None),
252                                 datetime(2011, 11, 6, 1, 30))
253
254                self.assertEqual(t1_tor.replace(tzinfo=None),
255                                 datetime(2011, 11, 6, 1, 30))
256
257                self.assertNotEqual(t0_tor.tzname(), t1_tor.tzname())
258                self.assertEqual(t0_tor.utcoffset(), timedelta(hours=-4.0))
259                self.assertEqual(t1_tor.utcoffset(), timedelta(hours=-5.0))
260
261    def testGapNegativeUTCOffset(self):
262        # Test that we don't have a problem around gaps.
263        tzname = self._get_tzname('America/Toronto')
264
265        with self._gettz_context(tzname):
266            TOR = self.gettz(tzname)
267
268            t0_u = datetime(2011, 3, 13, 6, 30, tzinfo=tz.tzutc())
269            t1_u = datetime(2011, 3, 13, 7, 30, tzinfo=tz.tzutc())
270
271            t0 = t0_u.astimezone(TOR)
272            t1 = t1_u.astimezone(TOR)
273
274            self.assertEqual(t0.replace(tzinfo=None),
275                             datetime(2011, 3, 13, 1, 30))
276
277            self.assertEqual(t1.replace(tzinfo=None),
278                             datetime(2011, 3, 13, 3, 30))
279
280            self.assertNotEqual(t0, t1)
281            self.assertEqual(t0.utcoffset(), timedelta(hours=-5.0))
282            self.assertEqual(t1.utcoffset(), timedelta(hours=-4.0))
283
284    def testFoldLondon(self):
285        tzname = self._get_tzname('Europe/London')
286
287        with self._gettz_context(tzname):
288            LON = self.gettz(tzname)
289            UTC = tz.tzutc()
290
291            t0_u = datetime(2013, 10, 27, 0, 30, tzinfo=UTC)   # BST
292            t1_u = datetime(2013, 10, 27, 1, 30, tzinfo=UTC)   # GMT
293
294            t0 = t0_u.astimezone(LON)
295            t1 = t1_u.astimezone(LON)
296
297            self.assertEqual(t0.replace(tzinfo=None),
298                             datetime(2013, 10, 27, 1, 30))
299
300            self.assertEqual(t1.replace(tzinfo=None),
301                             datetime(2013, 10, 27, 1, 30))
302
303            self.assertEqual(t0.utcoffset(), timedelta(hours=1))
304            self.assertEqual(t1.utcoffset(), timedelta(hours=0))
305
306    def testFoldIndependence(self):
307        tzname = self._get_tzname('America/New_York')
308
309        with self._gettz_context(tzname):
310            NYC = self.gettz(tzname)
311            UTC = tz.tzutc()
312            hour = timedelta(hours=1)
313
314            # Firmly 2015-11-01 0:30 EDT-4
315            pre_dst = datetime(2015, 11, 1, 0, 30, tzinfo=NYC)
316
317            # Ambiguous between 2015-11-01 1:30 EDT-4 and 2015-11-01 1:30 EST-5
318            in_dst = pre_dst + hour
319            in_dst_tzname_0 = in_dst.tzname()     # Stash the tzname - EDT
320
321            # Doing the arithmetic in UTC creates a date that is unambiguously
322            # 2015-11-01 1:30 EDT-5
323            in_dst_via_utc = (pre_dst.astimezone(UTC) + 2*hour).astimezone(NYC)
324
325            # Make sure the dates are actually ambiguous
326            self.assertEqual(in_dst, in_dst_via_utc)
327
328            # Make sure we got the right folding behavior
329            self.assertNotEqual(in_dst_via_utc.tzname(), in_dst_tzname_0)
330
331            # Now check to make sure in_dst's tzname hasn't changed
332            self.assertEqual(in_dst_tzname_0, in_dst.tzname())
333
334    def testInZoneFoldEquality(self):
335        # Two datetimes in the same zone are considered to be equal if their
336        # wall times are equal, even if they have different absolute times.
337
338        tzname = self._get_tzname('America/New_York')
339
340        with self._gettz_context(tzname):
341            NYC = self.gettz(tzname)
342            UTC = tz.tzutc()
343
344            dt0 = datetime(2011, 11, 6, 1, 30, tzinfo=NYC)
345            dt1 = tz.enfold(dt0, fold=1)
346
347            # Make sure these actually represent different times
348            self.assertNotEqual(dt0.astimezone(UTC), dt1.astimezone(UTC))
349
350            # Test that they compare equal
351            self.assertEqual(dt0, dt1)
352
353    def _test_ambiguous_time(self, dt, tzid, ambiguous):
354        # This is a test to check that the individual is_ambiguous values
355        # on the _tzinfo subclasses work.
356        tzname = self._get_tzname(tzid)
357
358        with self._gettz_context(tzname):
359            tzi = self.gettz(tzname)
360
361            self.assertEqual(tz.datetime_ambiguous(dt, tz=tzi), ambiguous)
362
363    def testAmbiguousNegativeUTCOffset(self):
364        self._test_ambiguous_time(datetime(2015, 11, 1, 1, 30),
365                                  'America/New_York', True)
366
367    def testAmbiguousPositiveUTCOffset(self):
368        self._test_ambiguous_time(datetime(2012, 4, 1, 2, 30),
369                                  'Australia/Sydney', True)
370
371    def testUnambiguousNegativeUTCOffset(self):
372        self._test_ambiguous_time(datetime(2015, 11, 1, 2, 30),
373                                  'America/New_York', False)
374
375    def testUnambiguousPositiveUTCOffset(self):
376        self._test_ambiguous_time(datetime(2012, 4, 1, 3, 30),
377                                  'Australia/Sydney', False)
378
379    def testUnambiguousGapNegativeUTCOffset(self):
380        # Imaginary time
381        self._test_ambiguous_time(datetime(2011, 3, 13, 2, 30),
382                                  'America/New_York', False)
383
384    def testUnambiguousGapPositiveUTCOffset(self):
385        # Imaginary time
386        self._test_ambiguous_time(datetime(2012, 10, 7, 2, 30),
387                                  'Australia/Sydney', False)
388
389    def _test_imaginary_time(self, dt, tzid, exists):
390        tzname = self._get_tzname(tzid)
391        with self._gettz_context(tzname):
392            tzi = self.gettz(tzname)
393
394            self.assertEqual(tz.datetime_exists(dt, tz=tzi), exists)
395
396    def testImaginaryNegativeUTCOffset(self):
397        self._test_imaginary_time(datetime(2011, 3, 13, 2, 30),
398                                  'America/New_York', False)
399
400    def testNotImaginaryNegativeUTCOffset(self):
401        self._test_imaginary_time(datetime(2011, 3, 13, 1, 30),
402                                  'America/New_York', True)
403
404    def testImaginaryPositiveUTCOffset(self):
405        self._test_imaginary_time(datetime(2012, 10, 7, 2, 30),
406                                  'Australia/Sydney', False)
407
408    def testNotImaginaryPositiveUTCOffset(self):
409        self._test_imaginary_time(datetime(2012, 10, 7, 1, 30),
410                                  'Australia/Sydney', True)
411
412    def testNotImaginaryFoldNegativeUTCOffset(self):
413        self._test_imaginary_time(datetime(2015, 11, 1, 1, 30),
414                                  'America/New_York', True)
415
416    def testNotImaginaryFoldPositiveUTCOffset(self):
417        self._test_imaginary_time(datetime(2012, 4, 1, 3, 30),
418                                  'Australia/Sydney', True)
419
420    @unittest.skip("Known failure in Python 3.6.")
421    def testEqualAmbiguousComparison(self):
422        tzname = self._get_tzname('Australia/Sydney')
423
424        with self._gettz_context(tzname):
425            SYD0 = self.gettz(tzname)
426            SYD1 = self.gettz(tzname)
427
428            t0_u = datetime(2012, 3, 31, 14, 30, tzinfo=tz.tzutc())  # AEST
429
430            t0_syd0 = t0_u.astimezone(SYD0)
431            t0_syd1 = t0_u.astimezone(SYD1)
432
433            # This is considered an "inter-zone comparison" because it's an
434            # ambiguous datetime.
435            self.assertEqual(t0_syd0, t0_syd1)
436
437
438class TzWinFoldMixin(object):
439    def get_args(self, tzname):
440        return (tzname, )
441
442    class context(object):
443        def __init__(*args, **kwargs):
444            pass
445
446        def __enter__(*args, **kwargs):
447            pass
448
449        def __exit__(*args, **kwargs):
450            pass
451
452    def get_utc_transitions(self, tzi, year, gap):
453        dston, dstoff = tzi.transitions(year)
454        if gap:
455            t_n = dston - timedelta(minutes=30)
456
457            t0_u = t_n.replace(tzinfo=tzi).astimezone(tz.tzutc())
458            t1_u = t0_u + timedelta(hours=1)
459        else:
460            # Get 1 hour before the first ambiguous date
461            t_n = dstoff - timedelta(minutes=30)
462
463            t0_u = t_n.replace(tzinfo=tzi).astimezone(tz.tzutc())
464            t_n += timedelta(hours=1)                   # Naive ambiguous date
465            t0_u = t0_u + timedelta(hours=1)            # First ambiguous date
466            t1_u = t0_u + timedelta(hours=1)            # Second ambiguous date
467
468        return t_n, t0_u, t1_u
469
470    def testFoldPositiveUTCOffset(self):
471        # Test that we can resolve ambiguous times
472        tzname = 'AUS Eastern Standard Time'
473        args = self.get_args(tzname)
474
475        with self.context(tzname):
476            # Calling fromutc() alters the tzfile object
477            SYD = self.tzclass(*args)
478
479            # Get the transition time in UTC from the object, because
480            # Windows doesn't store historical info
481            t_n, t0_u, t1_u = self.get_utc_transitions(SYD, 2012, False)
482
483            # Using fresh tzfiles
484            t0_syd = t0_u.astimezone(SYD)
485            t1_syd = t1_u.astimezone(SYD)
486
487            self.assertEqual(t0_syd.replace(tzinfo=None), t_n)
488
489            self.assertEqual(t1_syd.replace(tzinfo=None), t_n)
490
491            self.assertEqual(t0_syd.utcoffset(), timedelta(hours=11))
492            self.assertEqual(t1_syd.utcoffset(), timedelta(hours=10))
493            self.assertNotEqual(t0_syd.tzname(), t1_syd.tzname())
494
495    def testGapPositiveUTCOffset(self):
496        # Test that we don't have a problem around gaps.
497        tzname = 'AUS Eastern Standard Time'
498        args = self.get_args(tzname)
499
500        with self.context(tzname):
501            SYD = self.tzclass(*args)
502
503            t_n, t0_u, t1_u = self.get_utc_transitions(SYD, 2012, True)
504
505            t0 = t0_u.astimezone(SYD)
506            t1 = t1_u.astimezone(SYD)
507
508            self.assertEqual(t0.replace(tzinfo=None), t_n)
509
510            self.assertEqual(t1.replace(tzinfo=None), t_n + timedelta(hours=2))
511
512            self.assertEqual(t0.utcoffset(), timedelta(hours=10))
513            self.assertEqual(t1.utcoffset(), timedelta(hours=11))
514
515    def testFoldNegativeUTCOffset(self):
516        # Test that we can resolve ambiguous times
517        tzname = 'Eastern Standard Time'
518        args = self.get_args(tzname)
519
520        with self.context(tzname):
521            TOR = self.tzclass(*args)
522
523            t_n, t0_u, t1_u = self.get_utc_transitions(TOR, 2011, False)
524
525            t0_tor = t0_u.astimezone(TOR)
526            t1_tor = t1_u.astimezone(TOR)
527
528            self.assertEqual(t0_tor.replace(tzinfo=None), t_n)
529            self.assertEqual(t1_tor.replace(tzinfo=None), t_n)
530
531            self.assertNotEqual(t0_tor.tzname(), t1_tor.tzname())
532            self.assertEqual(t0_tor.utcoffset(), timedelta(hours=-4.0))
533            self.assertEqual(t1_tor.utcoffset(), timedelta(hours=-5.0))
534
535    def testGapNegativeUTCOffset(self):
536        # Test that we don't have a problem around gaps.
537        tzname = 'Eastern Standard Time'
538        args = self.get_args(tzname)
539
540        with self.context(tzname):
541            TOR = self.tzclass(*args)
542
543            t_n, t0_u, t1_u = self.get_utc_transitions(TOR, 2011, True)
544
545            t0 = t0_u.astimezone(TOR)
546            t1 = t1_u.astimezone(TOR)
547
548            self.assertEqual(t0.replace(tzinfo=None),
549                             t_n)
550
551            self.assertEqual(t1.replace(tzinfo=None),
552                             t_n + timedelta(hours=2))
553
554            self.assertNotEqual(t0.tzname(), t1.tzname())
555            self.assertEqual(t0.utcoffset(), timedelta(hours=-5.0))
556            self.assertEqual(t1.utcoffset(), timedelta(hours=-4.0))
557
558    def testFoldIndependence(self):
559        tzname = 'Eastern Standard Time'
560        args = self.get_args(tzname)
561
562        with self.context(tzname):
563            NYC = self.tzclass(*args)
564            UTC = tz.tzutc()
565            hour = timedelta(hours=1)
566
567            # Firmly 2015-11-01 0:30 EDT-4
568            t_n, t0_u, t1_u = self.get_utc_transitions(NYC, 2015, False)
569
570            pre_dst = (t_n - hour).replace(tzinfo=NYC)
571
572            # Currently, there's no way around the fact that this resolves to an
573            # ambiguous date, which defaults to EST. I'm not hard-coding in the
574            # answer, though, because the preferred behavior would be that this
575            # results in a time on the EDT side.
576
577            # Ambiguous between 2015-11-01 1:30 EDT-4 and 2015-11-01 1:30 EST-5
578            in_dst = pre_dst + hour
579            in_dst_tzname_0 = in_dst.tzname()     # Stash the tzname - EDT
580
581            # Doing the arithmetic in UTC creates a date that is unambiguously
582            # 2015-11-01 1:30 EDT-5
583            in_dst_via_utc = (pre_dst.astimezone(UTC) + 2*hour).astimezone(NYC)
584
585            # Make sure we got the right folding behavior
586            self.assertNotEqual(in_dst_via_utc.tzname(), in_dst_tzname_0)
587
588            # Now check to make sure in_dst's tzname hasn't changed
589            self.assertEqual(in_dst_tzname_0, in_dst.tzname())
590
591    def testInZoneFoldEquality(self):
592        # Two datetimes in the same zone are considered to be equal if their
593        # wall times are equal, even if they have different absolute times.
594        tzname = 'Eastern Standard Time'
595        args = self.get_args(tzname)
596
597        with self.context(tzname):
598            NYC = self.tzclass(*args)
599            UTC = tz.tzutc()
600
601            t_n, t0_u, t1_u = self.get_utc_transitions(NYC, 2011, False)
602
603            dt0 = t_n.replace(tzinfo=NYC)
604            dt1 = tz.enfold(dt0, fold=1)
605
606            # Make sure these actually represent different times
607            self.assertNotEqual(dt0.astimezone(UTC), dt1.astimezone(UTC))
608
609            # Test that they compare equal
610            self.assertEqual(dt0, dt1)
611
612###
613# Test Cases
614class TzUTCTest(unittest.TestCase):
615    def testSingleton(self):
616        UTC_0 = tz.tzutc()
617        UTC_1 = tz.tzutc()
618
619        self.assertIs(UTC_0, UTC_1)
620
621    def testOffset(self):
622        ct = datetime(2009, 4, 1, 12, 11, 13, tzinfo=tz.tzutc())
623
624        self.assertEqual(ct.utcoffset(), timedelta(seconds=0))
625
626    def testDst(self):
627        ct = datetime(2009, 4, 1, 12, 11, 13, tzinfo=tz.tzutc())
628
629        self.assertEqual(ct.dst(), timedelta(seconds=0))
630
631    def testTzName(self):
632        ct = datetime(2009, 4, 1, 12, 11, 13, tzinfo=tz.tzutc())
633        self.assertEqual(ct.tzname(), 'UTC')
634
635    def testEquality(self):
636        UTC0 = tz.tzutc()
637        UTC1 = tz.tzutc()
638
639        self.assertEqual(UTC0, UTC1)
640
641    def testInequality(self):
642        UTC = tz.tzutc()
643        UTCp4 = tz.tzoffset('UTC+4', 14400)
644
645        self.assertNotEqual(UTC, UTCp4)
646
647    def testInequalityInteger(self):
648        self.assertFalse(tz.tzutc() == 7)
649        self.assertNotEqual(tz.tzutc(), 7)
650
651    def testInequalityUnsupported(self):
652        self.assertEqual(tz.tzutc(), ComparesEqual)
653
654    def testRepr(self):
655        UTC = tz.tzutc()
656        self.assertEqual(repr(UTC), 'tzutc()')
657
658    def testTimeOnlyUTC(self):
659        # https://github.com/dateutil/dateutil/issues/132
660        # tzutc doesn't care
661        tz_utc = tz.tzutc()
662        self.assertEqual(dt_time(13, 20, tzinfo=tz_utc).utcoffset(),
663                         timedelta(0))
664
665    def testAmbiguity(self):
666        # Pick an arbitrary datetime, this should always return False.
667        dt = datetime(2011, 9, 1, 2, 30, tzinfo=tz.tzutc())
668
669        self.assertFalse(tz.datetime_ambiguous(dt))
670
671
672@pytest.mark.tzoffset
673class TzOffsetTest(unittest.TestCase):
674    def testTimedeltaOffset(self):
675        est = tz.tzoffset('EST', timedelta(hours=-5))
676        est_s = tz.tzoffset('EST', -18000)
677
678        self.assertEqual(est, est_s)
679
680    def testTzNameNone(self):
681        gmt5 = tz.tzoffset(None, -18000)       # -5:00
682        self.assertIs(datetime(2003, 10, 26, 0, 0, tzinfo=gmt5).tzname(),
683                      None)
684
685    def testTimeOnlyOffset(self):
686        # tzoffset doesn't care
687        tz_offset = tz.tzoffset('+3', 3600)
688        self.assertEqual(dt_time(13, 20, tzinfo=tz_offset).utcoffset(),
689                         timedelta(seconds=3600))
690
691    def testTzOffsetRepr(self):
692        tname = 'EST'
693        tzo = tz.tzoffset(tname, -5 * 3600)
694        self.assertEqual(repr(tzo), "tzoffset(" + repr(tname) + ", -18000)")
695
696    def testEquality(self):
697        utc = tz.tzoffset('UTC', 0)
698        gmt = tz.tzoffset('GMT', 0)
699
700        self.assertEqual(utc, gmt)
701
702    def testUTCEquality(self):
703        utc = tz.tzutc()
704        o_utc = tz.tzoffset('UTC', 0)
705
706        self.assertEqual(utc, o_utc)
707        self.assertEqual(o_utc, utc)
708
709    def testInequalityInvalid(self):
710        tzo = tz.tzoffset('-3', -3 * 3600)
711        self.assertFalse(tzo == -3)
712        self.assertNotEqual(tzo, -3)
713
714    def testInequalityUnsupported(self):
715        tzo = tz.tzoffset('-5', -5 * 3600)
716
717        self.assertTrue(tzo == ComparesEqual)
718        self.assertFalse(tzo != ComparesEqual)
719        self.assertEqual(tzo, ComparesEqual)
720
721    def testAmbiguity(self):
722        # Pick an arbitrary datetime, this should always return False.
723        dt = datetime(2011, 9, 1, 2, 30, tzinfo=tz.tzoffset("EST", -5 * 3600))
724
725        self.assertFalse(tz.datetime_ambiguous(dt))
726
727    def testTzOffsetInstance(self):
728        tz1 = tz.tzoffset.instance('EST', timedelta(hours=-5))
729        tz2 = tz.tzoffset.instance('EST', timedelta(hours=-5))
730
731        assert tz1 is not tz2
732
733    def testTzOffsetSingletonDifferent(self):
734        tz1 = tz.tzoffset('EST', timedelta(hours=-5))
735        tz2 = tz.tzoffset('EST', -18000)
736
737        assert tz1 is tz2
738
739
740@pytest.mark.smoke
741@pytest.mark.tzoffset
742def test_tzoffset_weakref():
743    UTC1 = tz.tzoffset('UTC', 0)
744    UTC_ref = weakref.ref(tz.tzoffset('UTC', 0))
745    UTC1 is UTC_ref()
746    del UTC1
747    gc.collect()
748
749    assert UTC_ref() is not None    # Should be in the strong cache
750    assert UTC_ref() is tz.tzoffset('UTC', 0)
751
752    # Fill the strong cache with other items
753    for offset in range(5,15):
754        tz.tzoffset('RandomZone', offset)
755
756    gc.collect()
757    assert UTC_ref() is  None
758    assert UTC_ref() is not tz.tzoffset('UTC', 0)
759
760
761@pytest.mark.tzoffset
762@pytest.mark.parametrize('args', [
763    ('UTC', 0),
764    ('EST', -18000),
765    ('EST', timedelta(hours=-5)),
766    (None, timedelta(hours=3)),
767])
768def test_tzoffset_singleton(args):
769    tz1 = tz.tzoffset(*args)
770    tz2 = tz.tzoffset(*args)
771
772    assert tz1 is tz2
773
774
775@pytest.mark.tzoffset
776@pytest.mark.skipif(not SUPPORTS_SUB_MINUTE_OFFSETS,
777                    reason='Sub-minute offsets not supported')
778def test_tzoffset_sub_minute():
779    delta = timedelta(hours=12, seconds=30)
780    test_datetime = datetime(2000, 1, 1, tzinfo=tz.tzoffset(None, delta))
781    assert test_datetime.utcoffset() == delta
782
783
784@pytest.mark.tzoffset
785@pytest.mark.skipif(SUPPORTS_SUB_MINUTE_OFFSETS,
786                    reason='Sub-minute offsets supported')
787def test_tzoffset_sub_minute_rounding():
788    delta = timedelta(hours=12, seconds=30)
789    test_date = datetime(2000, 1, 1, tzinfo=tz.tzoffset(None, delta))
790    assert test_date.utcoffset() == timedelta(hours=12, minutes=1)
791
792
793@pytest.mark.tzlocal
794class TzLocalTest(unittest.TestCase):
795    def testEquality(self):
796        tz1 = tz.tzlocal()
797        tz2 = tz.tzlocal()
798
799        # Explicitly calling == and != here to ensure the operators work
800        self.assertTrue(tz1 == tz2)
801        self.assertFalse(tz1 != tz2)
802
803    def testInequalityFixedOffset(self):
804        tzl = tz.tzlocal()
805        tzos = tz.tzoffset('LST', tzl._std_offset.total_seconds())
806        tzod = tz.tzoffset('LDT', tzl._std_offset.total_seconds())
807
808        self.assertFalse(tzl == tzos)
809        self.assertFalse(tzl == tzod)
810        self.assertTrue(tzl != tzos)
811        self.assertTrue(tzl != tzod)
812
813    def testInequalityInvalid(self):
814        tzl = tz.tzlocal()
815
816        self.assertTrue(tzl != 1)
817        self.assertFalse(tzl == 1)
818
819        # TODO: Use some sort of universal local mocking so that it's clear
820        # that we're expecting tzlocal to *not* be Pacific/Kiritimati
821        LINT = tz.gettz('Pacific/Kiritimati')
822        self.assertTrue(tzl != LINT)
823        self.assertFalse(tzl == LINT)
824
825    def testInequalityUnsupported(self):
826        tzl = tz.tzlocal()
827
828        self.assertTrue(tzl == ComparesEqual)
829        self.assertFalse(tzl != ComparesEqual)
830
831    def testRepr(self):
832        tzl = tz.tzlocal()
833
834        self.assertEqual(repr(tzl), 'tzlocal()')
835
836
837@pytest.mark.parametrize('args,kwargs', [
838    (('EST', -18000), {}),
839    (('EST', timedelta(hours=-5)), {}),
840    (('EST',), {'offset': -18000}),
841    (('EST',), {'offset': timedelta(hours=-5)}),
842    (tuple(), {'name': 'EST', 'offset': -18000})
843])
844def test_tzoffset_is(args, kwargs):
845    tz_ref = tz.tzoffset('EST', -18000)
846    assert tz.tzoffset(*args, **kwargs) is tz_ref
847
848
849def test_tzoffset_is_not():
850    assert tz.tzoffset('EDT', -14400) is not tz.tzoffset('EST', -18000)
851
852
853@pytest.mark.tzlocal
854@unittest.skipIf(IS_WIN, "requires Unix")
855@unittest.skipUnless(TZEnvContext.tz_change_allowed(),
856                     TZEnvContext.tz_change_disallowed_message())
857class TzLocalNixTest(unittest.TestCase, TzFoldMixin):
858    # This is a set of tests for `tzlocal()` on *nix systems
859
860    # POSIX string indicating change to summer time on the 2nd Sunday in March
861    # at 2AM, and ending the 1st Sunday in November at 2AM. (valid >= 2007)
862    TZ_EST = 'EST+5EDT,M3.2.0/2,M11.1.0/2'
863
864    # POSIX string for AEST/AEDT (valid >= 2008)
865    TZ_AEST = 'AEST-10AEDT,M10.1.0/2,M4.1.0/3'
866
867    # POSIX string for BST/GMT
868    TZ_LON = 'GMT0BST,M3.5.0,M10.5.0'
869
870    # POSIX string for UTC
871    UTC = 'UTC'
872
873    def gettz(self, tzname):
874        # Actual time zone changes are handled by the _gettz_context function
875        return tz.tzlocal()
876
877    def _gettz_context(self, tzname):
878        tzname_map = {'Australia/Sydney': self.TZ_AEST,
879                      'America/Toronto': self.TZ_EST,
880                      'America/New_York': self.TZ_EST,
881                      'Europe/London': self.TZ_LON}
882
883        return TZEnvContext(tzname_map.get(tzname, tzname))
884
885    def _testTzFunc(self, tzval, func, std_val, dst_val):
886        """
887        This generates tests about how the behavior of a function ``func``
888        changes between STD and DST (e.g. utcoffset, tzname, dst).
889
890        It assume that DST starts the 2nd Sunday in March and ends the 1st
891        Sunday in November
892        """
893        with TZEnvContext(tzval):
894            dt1 = datetime(2015, 2, 1, 12, 0, tzinfo=tz.tzlocal())  # STD
895            dt2 = datetime(2015, 5, 1, 12, 0, tzinfo=tz.tzlocal())  # DST
896
897            self.assertEqual(func(dt1), std_val)
898            self.assertEqual(func(dt2), dst_val)
899
900    def _testTzName(self, tzval, std_name, dst_name):
901        func = datetime.tzname
902
903        self._testTzFunc(tzval, func, std_name, dst_name)
904
905    def testTzNameDST(self):
906        # Test tzname in a zone with DST
907        self._testTzName(self.TZ_EST, 'EST', 'EDT')
908
909    def testTzNameUTC(self):
910        # Test tzname in a zone without DST
911        self._testTzName(self.UTC, 'UTC', 'UTC')
912
913    def _testOffset(self, tzval, std_off, dst_off):
914        func = datetime.utcoffset
915
916        self._testTzFunc(tzval, func, std_off, dst_off)
917
918    def testOffsetDST(self):
919        self._testOffset(self.TZ_EST, timedelta(hours=-5), timedelta(hours=-4))
920
921    def testOffsetUTC(self):
922        self._testOffset(self.UTC, timedelta(0), timedelta(0))
923
924    def _testDST(self, tzval, dst_dst):
925        func = datetime.dst
926        std_dst = timedelta(0)
927
928        self._testTzFunc(tzval, func, std_dst, dst_dst)
929
930    def testDSTDST(self):
931        self._testDST(self.TZ_EST, timedelta(hours=1))
932
933    def testDSTUTC(self):
934        self._testDST(self.UTC, timedelta(0))
935
936    def testTimeOnlyOffsetLocalUTC(self):
937        with TZEnvContext(self.UTC):
938            self.assertEqual(dt_time(13, 20, tzinfo=tz.tzlocal()).utcoffset(),
939                             timedelta(0))
940
941    def testTimeOnlyOffsetLocalDST(self):
942        with TZEnvContext(self.TZ_EST):
943            self.assertIs(dt_time(13, 20, tzinfo=tz.tzlocal()).utcoffset(),
944                          None)
945
946    def testTimeOnlyDSTLocalUTC(self):
947        with TZEnvContext(self.UTC):
948            self.assertEqual(dt_time(13, 20, tzinfo=tz.tzlocal()).dst(),
949                             timedelta(0))
950
951    def testTimeOnlyDSTLocalDST(self):
952        with TZEnvContext(self.TZ_EST):
953            self.assertIs(dt_time(13, 20, tzinfo=tz.tzlocal()).dst(),
954                          None)
955
956    def testUTCEquality(self):
957        with TZEnvContext(self.UTC):
958            assert tz.tzlocal() == tz.tzutc()
959
960
961# TODO: Maybe a better hack than this?
962def mark_tzlocal_nix(f):
963    marks = [
964        pytest.mark.tzlocal,
965        pytest.mark.skipif(IS_WIN, reason='requires Unix'),
966        pytest.mark.skipif(not TZEnvContext.tz_change_allowed,
967                           reason=TZEnvContext.tz_change_disallowed_message())
968    ]
969
970    for mark in reversed(marks):
971        f = mark(f)
972
973    return f
974
975
976@mark_tzlocal_nix
977@pytest.mark.parametrize('tzvar', ['UTC', 'GMT0', 'UTC0'])
978def test_tzlocal_utc_equal(tzvar):
979    with TZEnvContext(tzvar):
980        assert tz.tzlocal() == tz.UTC
981
982
983@mark_tzlocal_nix
984@pytest.mark.parametrize('tzvar', [
985    'Europe/London', 'America/New_York',
986    'GMT0BST', 'EST5EDT'])
987def test_tzlocal_utc_unequal(tzvar):
988    with TZEnvContext(tzvar):
989        assert tz.tzlocal() != tz.UTC
990
991
992@mark_tzlocal_nix
993def test_tzlocal_local_time_trim_colon():
994    with TZEnvContext(':/etc/localtime'):
995        assert tz.gettz() is not None
996
997
998@mark_tzlocal_nix
999@pytest.mark.parametrize('tzvar, tzoff', [
1000    ('EST5', tz.tzoffset('EST', -18000)),
1001    ('GMT', tz.tzoffset('GMT', 0)),
1002    ('YAKT-9', tz.tzoffset('YAKT', timedelta(hours=9))),
1003    ('JST-9', tz.tzoffset('JST', timedelta(hours=9))),
1004])
1005def test_tzlocal_offset_equal(tzvar, tzoff):
1006    with TZEnvContext(tzvar):
1007        # Including both to test both __eq__ and __ne__
1008        assert tz.tzlocal() == tzoff
1009        assert not (tz.tzlocal() != tzoff)
1010
1011
1012@mark_tzlocal_nix
1013@pytest.mark.parametrize('tzvar, tzoff', [
1014    ('EST5EDT', tz.tzoffset('EST', -18000)),
1015    ('GMT0BST', tz.tzoffset('GMT', 0)),
1016    ('EST5', tz.tzoffset('EST', -14400)),
1017    ('YAKT-9', tz.tzoffset('JST', timedelta(hours=9))),
1018    ('JST-9', tz.tzoffset('YAKT', timedelta(hours=9))),
1019])
1020def test_tzlocal_offset_unequal(tzvar, tzoff):
1021    with TZEnvContext(tzvar):
1022        # Including both to test both __eq__ and __ne__
1023        assert tz.tzlocal() != tzoff
1024        assert not (tz.tzlocal() == tzoff)
1025
1026
1027@pytest.mark.gettz
1028class GettzTest(unittest.TestCase, TzFoldMixin):
1029    gettz = staticmethod(tz.gettz)
1030
1031    def testGettz(self):
1032        # bug 892569
1033        str(self.gettz('UTC'))
1034
1035    def testGetTzEquality(self):
1036        self.assertEqual(self.gettz('UTC'), self.gettz('UTC'))
1037
1038    def testTimeOnlyGettz(self):
1039        # gettz returns None
1040        tz_get = self.gettz('Europe/Minsk')
1041        self.assertIs(dt_time(13, 20, tzinfo=tz_get).utcoffset(), None)
1042
1043    def testTimeOnlyGettzDST(self):
1044        # gettz returns None
1045        tz_get = self.gettz('Europe/Minsk')
1046        self.assertIs(dt_time(13, 20, tzinfo=tz_get).dst(), None)
1047
1048    def testTimeOnlyGettzTzName(self):
1049        tz_get = self.gettz('Europe/Minsk')
1050        self.assertIs(dt_time(13, 20, tzinfo=tz_get).tzname(), None)
1051
1052    def testTimeOnlyFormatZ(self):
1053        tz_get = self.gettz('Europe/Minsk')
1054        t = dt_time(13, 20, tzinfo=tz_get)
1055
1056        self.assertEqual(t.strftime('%H%M%Z'), '1320')
1057
1058    def testPortugalDST(self):
1059        # In 1996, Portugal changed from CET to WET
1060        PORTUGAL = self.gettz('Portugal')
1061
1062        t_cet = datetime(1996, 3, 31, 1, 59, tzinfo=PORTUGAL)
1063
1064        self.assertEqual(t_cet.tzname(), 'CET')
1065        self.assertEqual(t_cet.utcoffset(), timedelta(hours=1))
1066        self.assertEqual(t_cet.dst(), timedelta(0))
1067
1068        t_west = datetime(1996, 3, 31, 2, 1, tzinfo=PORTUGAL)
1069
1070        self.assertEqual(t_west.tzname(), 'WEST')
1071        self.assertEqual(t_west.utcoffset(), timedelta(hours=1))
1072        self.assertEqual(t_west.dst(), timedelta(hours=1))
1073
1074    def testGettzCacheTzFile(self):
1075        NYC1 = tz.gettz('America/New_York')
1076        NYC2 = tz.gettz('America/New_York')
1077
1078        assert NYC1 is NYC2
1079
1080    def testGettzCacheTzLocal(self):
1081        local1 = tz.gettz()
1082        local2 = tz.gettz()
1083
1084        assert local1 is not local2
1085
1086
1087@pytest.mark.gettz
1088@pytest.mark.parametrize('badzone', [
1089    'Fake.Region/Abcdefghijklmnop',  # Violates several tz project name rules
1090])
1091def test_gettz_badzone(badzone):
1092    # Make sure passing a bad TZ string to gettz returns None (GH #800)
1093    tzi = tz.gettz(badzone)
1094    assert tzi is None
1095
1096
1097@pytest.mark.gettz
1098def test_gettz_badzone_unicode():
1099    # Make sure a unicode string can be passed to TZ (GH #802)
1100    # When fixed, combine this with test_gettz_badzone
1101    tzi = tz.gettz('��')
1102    assert tzi is None
1103
1104
1105@pytest.mark.gettz
1106@pytest.mark.xfail(IS_WIN, reason='zoneinfo separately cached')
1107def test_gettz_cache_clear():
1108    NYC1 = tz.gettz('America/New_York')
1109    tz.gettz.cache_clear()
1110
1111    NYC2 = tz.gettz('America/New_York')
1112
1113    assert NYC1 is not NYC2
1114
1115@pytest.mark.gettz
1116@pytest.mark.xfail(IS_WIN, reason='zoneinfo separately cached')
1117def test_gettz_set_cache_size():
1118    tz.gettz.cache_clear()
1119    tz.gettz.set_cache_size(3)
1120
1121    MONACO_ref = weakref.ref(tz.gettz('Europe/Monaco'))
1122    EASTER_ref = weakref.ref(tz.gettz('Pacific/Easter'))
1123    CURRIE_ref = weakref.ref(tz.gettz('Australia/Currie'))
1124
1125    gc.collect()
1126
1127    assert MONACO_ref() is not None
1128    assert EASTER_ref() is not None
1129    assert CURRIE_ref() is not None
1130
1131    tz.gettz.set_cache_size(2)
1132    gc.collect()
1133
1134    assert MONACO_ref() is None
1135
1136@pytest.mark.xfail(IS_WIN, reason="Windows does not use system zoneinfo")
1137@pytest.mark.smoke
1138@pytest.mark.gettz
1139def test_gettz_weakref():
1140    tz.gettz.cache_clear()
1141    tz.gettz.set_cache_size(2)
1142    NYC1 = tz.gettz('America/New_York')
1143    NYC_ref = weakref.ref(tz.gettz('America/New_York'))
1144
1145    assert NYC1 is NYC_ref()
1146
1147    del NYC1
1148    gc.collect()
1149
1150    assert NYC_ref() is not None        # Should still be in the strong cache
1151    assert tz.gettz('America/New_York') is NYC_ref()
1152
1153    # Populate strong cache with other timezones
1154    tz.gettz('Europe/Monaco')
1155    tz.gettz('Pacific/Easter')
1156    tz.gettz('Australia/Currie')
1157
1158    gc.collect()
1159    assert NYC_ref() is None    # Should have been pushed out
1160    assert tz.gettz('America/New_York') is not NYC_ref()
1161
1162class ZoneInfoGettzTest(GettzTest, WarningTestMixin):
1163    def gettz(self, name):
1164        zoneinfo_file = zoneinfo.get_zonefile_instance()
1165        return zoneinfo_file.get(name)
1166
1167    def testZoneInfoFileStart1(self):
1168        tz = self.gettz("EST5EDT")
1169        self.assertEqual(datetime(2003, 4, 6, 1, 59, tzinfo=tz).tzname(), "EST",
1170                         MISSING_TARBALL)
1171        self.assertEqual(datetime(2003, 4, 6, 2, 00, tzinfo=tz).tzname(), "EDT")
1172
1173    def testZoneInfoFileEnd1(self):
1174        tzc = self.gettz("EST5EDT")
1175        self.assertEqual(datetime(2003, 10, 26, 0, 59, tzinfo=tzc).tzname(),
1176                         "EDT", MISSING_TARBALL)
1177
1178        end_est = tz.enfold(datetime(2003, 10, 26, 1, 00, tzinfo=tzc), fold=1)
1179        self.assertEqual(end_est.tzname(), "EST")
1180
1181    def testZoneInfoOffsetSignal(self):
1182        utc = self.gettz("UTC")
1183        nyc = self.gettz("America/New_York")
1184        self.assertNotEqual(utc, None, MISSING_TARBALL)
1185        self.assertNotEqual(nyc, None)
1186        t0 = datetime(2007, 11, 4, 0, 30, tzinfo=nyc)
1187        t1 = t0.astimezone(utc)
1188        t2 = t1.astimezone(nyc)
1189        self.assertEqual(t0, t2)
1190        self.assertEqual(nyc.dst(t0), timedelta(hours=1))
1191
1192    def testZoneInfoCopy(self):
1193        # copy.copy() called on a ZoneInfo file was returning the same instance
1194        CHI = self.gettz('America/Chicago')
1195        CHI_COPY = copy.copy(CHI)
1196
1197        self.assertIsNot(CHI, CHI_COPY)
1198        self.assertEqual(CHI, CHI_COPY)
1199
1200    def testZoneInfoDeepCopy(self):
1201        CHI = self.gettz('America/Chicago')
1202        CHI_COPY = copy.deepcopy(CHI)
1203
1204        self.assertIsNot(CHI, CHI_COPY)
1205        self.assertEqual(CHI, CHI_COPY)
1206
1207    def testZoneInfoInstanceCaching(self):
1208        zif_0 = zoneinfo.get_zonefile_instance()
1209        zif_1 = zoneinfo.get_zonefile_instance()
1210
1211        self.assertIs(zif_0, zif_1)
1212
1213    def testZoneInfoNewInstance(self):
1214        zif_0 = zoneinfo.get_zonefile_instance()
1215        zif_1 = zoneinfo.get_zonefile_instance(new_instance=True)
1216        zif_2 = zoneinfo.get_zonefile_instance()
1217
1218        self.assertIsNot(zif_0, zif_1)
1219        self.assertIs(zif_1, zif_2)
1220
1221    def testZoneInfoDeprecated(self):
1222        with self.assertWarns(DeprecationWarning):
1223            zoneinfo.gettz('US/Eastern')
1224
1225    def testZoneInfoMetadataDeprecated(self):
1226        with self.assertWarns(DeprecationWarning):
1227            zoneinfo.gettz_db_metadata()
1228
1229
1230class TZRangeTest(unittest.TestCase, TzFoldMixin):
1231    TZ_EST = tz.tzrange('EST', timedelta(hours=-5),
1232                        'EDT', timedelta(hours=-4),
1233                        start=relativedelta(month=3, day=1, hour=2,
1234                                            weekday=SU(+2)),
1235                        end=relativedelta(month=11, day=1, hour=1,
1236                                          weekday=SU(+1)))
1237
1238    TZ_AEST = tz.tzrange('AEST', timedelta(hours=10),
1239                         'AEDT', timedelta(hours=11),
1240                         start=relativedelta(month=10, day=1, hour=2,
1241                                             weekday=SU(+1)),
1242                         end=relativedelta(month=4, day=1, hour=2,
1243                                           weekday=SU(+1)))
1244
1245    TZ_LON = tz.tzrange('GMT', timedelta(hours=0),
1246                        'BST', timedelta(hours=1),
1247                        start=relativedelta(month=3, day=31, weekday=SU(-1),
1248                                            hours=2),
1249                        end=relativedelta(month=10, day=31, weekday=SU(-1),
1250                                          hours=1))
1251    # POSIX string for UTC
1252    UTC = 'UTC'
1253
1254    def gettz(self, tzname):
1255        tzname_map = {'Australia/Sydney': self.TZ_AEST,
1256                      'America/Toronto': self.TZ_EST,
1257                      'America/New_York': self.TZ_EST,
1258                      'Europe/London': self.TZ_LON}
1259
1260        return tzname_map[tzname]
1261
1262    def testRangeCmp1(self):
1263        self.assertEqual(tz.tzstr("EST5EDT"),
1264                         tz.tzrange("EST", -18000, "EDT", -14400,
1265                                 relativedelta(hours=+2,
1266                                               month=4, day=1,
1267                                               weekday=SU(+1)),
1268                                 relativedelta(hours=+1,
1269                                               month=10, day=31,
1270                                               weekday=SU(-1))))
1271
1272    def testRangeCmp2(self):
1273        self.assertEqual(tz.tzstr("EST5EDT"),
1274                         tz.tzrange("EST", -18000, "EDT"))
1275
1276    def testRangeOffsets(self):
1277        TZR = tz.tzrange('EST', -18000, 'EDT', -14400,
1278                         start=relativedelta(hours=2, month=4, day=1,
1279                                             weekday=SU(+2)),
1280                         end=relativedelta(hours=1, month=10, day=31,
1281                                           weekday=SU(-1)))
1282
1283        dt_std = datetime(2014, 4, 11, 12, 0, tzinfo=TZR)  # STD
1284        dt_dst = datetime(2016, 4, 11, 12, 0, tzinfo=TZR)  # DST
1285
1286        dst_zero = timedelta(0)
1287        dst_hour = timedelta(hours=1)
1288
1289        std_offset = timedelta(hours=-5)
1290        dst_offset = timedelta(hours=-4)
1291
1292        # Check dst()
1293        self.assertEqual(dt_std.dst(), dst_zero)
1294        self.assertEqual(dt_dst.dst(), dst_hour)
1295
1296        # Check utcoffset()
1297        self.assertEqual(dt_std.utcoffset(), std_offset)
1298        self.assertEqual(dt_dst.utcoffset(), dst_offset)
1299
1300        # Check tzname
1301        self.assertEqual(dt_std.tzname(), 'EST')
1302        self.assertEqual(dt_dst.tzname(), 'EDT')
1303
1304    def testTimeOnlyRangeFixed(self):
1305        # This is a fixed-offset zone, so tzrange allows this
1306        tz_range = tz.tzrange('dflt', stdoffset=timedelta(hours=-3))
1307        self.assertEqual(dt_time(13, 20, tzinfo=tz_range).utcoffset(),
1308                         timedelta(hours=-3))
1309
1310    def testTimeOnlyRange(self):
1311        # tzrange returns None because this zone has DST
1312        tz_range = tz.tzrange('EST', timedelta(hours=-5),
1313                              'EDT', timedelta(hours=-4))
1314        self.assertIs(dt_time(13, 20, tzinfo=tz_range).utcoffset(), None)
1315
1316    def testBrokenIsDstHandling(self):
1317        # tzrange._isdst() was using a date() rather than a datetime().
1318        # Issue reported by Lennart Regebro.
1319        dt = datetime(2007, 8, 6, 4, 10, tzinfo=tz.tzutc())
1320        self.assertEqual(dt.astimezone(tz=tz.gettz("GMT+2")),
1321                          datetime(2007, 8, 6, 6, 10, tzinfo=tz.tzstr("GMT+2")))
1322
1323    def testRangeTimeDelta(self):
1324        # Test that tzrange can be specified with a timedelta instead of an int.
1325        EST5EDT_td = tz.tzrange('EST', timedelta(hours=-5),
1326                                'EDT', timedelta(hours=-4))
1327
1328        EST5EDT_sec = tz.tzrange('EST', -18000,
1329                                 'EDT', -14400)
1330
1331        self.assertEqual(EST5EDT_td, EST5EDT_sec)
1332
1333    def testRangeEquality(self):
1334        TZR1 = tz.tzrange('EST', -18000, 'EDT', -14400)
1335
1336        # Standard abbreviation different
1337        TZR2 = tz.tzrange('ET', -18000, 'EDT', -14400)
1338        self.assertNotEqual(TZR1, TZR2)
1339
1340        # DST abbreviation different
1341        TZR3 = tz.tzrange('EST', -18000, 'EMT', -14400)
1342        self.assertNotEqual(TZR1, TZR3)
1343
1344        # STD offset different
1345        TZR4 = tz.tzrange('EST', -14000, 'EDT', -14400)
1346        self.assertNotEqual(TZR1, TZR4)
1347
1348        # DST offset different
1349        TZR5 = tz.tzrange('EST', -18000, 'EDT', -18000)
1350        self.assertNotEqual(TZR1, TZR5)
1351
1352        # Start delta different
1353        TZR6 = tz.tzrange('EST', -18000, 'EDT', -14400,
1354                          start=relativedelta(hours=+1, month=3,
1355                                              day=1, weekday=SU(+2)))
1356        self.assertNotEqual(TZR1, TZR6)
1357
1358        # End delta different
1359        TZR7 = tz.tzrange('EST', -18000, 'EDT', -14400,
1360            end=relativedelta(hours=+1, month=11,
1361                              day=1, weekday=SU(+2)))
1362        self.assertNotEqual(TZR1, TZR7)
1363
1364    def testRangeInequalityUnsupported(self):
1365        TZR = tz.tzrange('EST', -18000, 'EDT', -14400)
1366
1367        self.assertFalse(TZR == 4)
1368        self.assertTrue(TZR == ComparesEqual)
1369        self.assertFalse(TZR != ComparesEqual)
1370
1371
1372@pytest.mark.tzstr
1373class TZStrTest(unittest.TestCase, TzFoldMixin):
1374    # POSIX string indicating change to summer time on the 2nd Sunday in March
1375    # at 2AM, and ending the 1st Sunday in November at 2AM. (valid >= 2007)
1376    TZ_EST = 'EST+5EDT,M3.2.0/2,M11.1.0/2'
1377
1378    # POSIX string for AEST/AEDT (valid >= 2008)
1379    TZ_AEST = 'AEST-10AEDT,M10.1.0/2,M4.1.0/3'
1380
1381    # POSIX string for GMT/BST
1382    TZ_LON = 'GMT0BST,M3.5.0,M10.5.0'
1383
1384    def gettz(self, tzname):
1385        # Actual time zone changes are handled by the _gettz_context function
1386        tzname_map = {'Australia/Sydney': self.TZ_AEST,
1387                      'America/Toronto': self.TZ_EST,
1388                      'America/New_York': self.TZ_EST,
1389                      'Europe/London': self.TZ_LON}
1390
1391        return tz.tzstr(tzname_map[tzname])
1392
1393    def testStrStr(self):
1394        # Test that tz.tzstr() won't throw an error if given a str instead
1395        # of a unicode literal.
1396        self.assertEqual(datetime(2003, 4, 6, 1, 59,
1397                                  tzinfo=tz.tzstr(str("EST5EDT"))).tzname(), "EST")
1398        self.assertEqual(datetime(2003, 4, 6, 2, 00,
1399                                  tzinfo=tz.tzstr(str("EST5EDT"))).tzname(), "EDT")
1400
1401    def testStrInequality(self):
1402        TZS1 = tz.tzstr('EST5EDT4')
1403
1404        # Standard abbreviation different
1405        TZS2 = tz.tzstr('ET5EDT4')
1406        self.assertNotEqual(TZS1, TZS2)
1407
1408        # DST abbreviation different
1409        TZS3 = tz.tzstr('EST5EMT')
1410        self.assertNotEqual(TZS1, TZS3)
1411
1412        # STD offset different
1413        TZS4 = tz.tzstr('EST4EDT4')
1414        self.assertNotEqual(TZS1, TZS4)
1415
1416        # DST offset different
1417        TZS5 = tz.tzstr('EST5EDT3')
1418        self.assertNotEqual(TZS1, TZS5)
1419
1420    def testStrInequalityStartEnd(self):
1421        TZS1 = tz.tzstr('EST5EDT4')
1422
1423        # Start delta different
1424        TZS2 = tz.tzstr('EST5EDT4,M4.2.0/02:00:00,M10-5-0/02:00')
1425        self.assertNotEqual(TZS1, TZS2)
1426
1427        # End delta different
1428        TZS3 = tz.tzstr('EST5EDT4,M4.2.0/02:00:00,M11-5-0/02:00')
1429        self.assertNotEqual(TZS1, TZS3)
1430
1431    def testPosixOffset(self):
1432        TZ1 = tz.tzstr('UTC-3')
1433        self.assertEqual(datetime(2015, 1, 1, tzinfo=TZ1).utcoffset(),
1434                         timedelta(hours=-3))
1435
1436        TZ2 = tz.tzstr('UTC-3', posix_offset=True)
1437        self.assertEqual(datetime(2015, 1, 1, tzinfo=TZ2).utcoffset(),
1438                         timedelta(hours=+3))
1439
1440    def testStrInequalityUnsupported(self):
1441        TZS = tz.tzstr('EST5EDT')
1442
1443        self.assertFalse(TZS == 4)
1444        self.assertTrue(TZS == ComparesEqual)
1445        self.assertFalse(TZS != ComparesEqual)
1446
1447    def testTzStrRepr(self):
1448        TZS1 = tz.tzstr('EST5EDT4')
1449        TZS2 = tz.tzstr('EST')
1450
1451        self.assertEqual(repr(TZS1), "tzstr(" + repr('EST5EDT4') + ")")
1452        self.assertEqual(repr(TZS2), "tzstr(" + repr('EST') + ")")
1453
1454    def testTzStrFailure(self):
1455        with self.assertRaises(ValueError):
1456            tz.tzstr('InvalidString;439999')
1457
1458    def testTzStrSingleton(self):
1459        tz1 = tz.tzstr('EST5EDT')
1460        tz2 = tz.tzstr('CST4CST')
1461        tz3 = tz.tzstr('EST5EDT')
1462
1463        self.assertIsNot(tz1, tz2)
1464        self.assertIs(tz1, tz3)
1465
1466    def testTzStrSingletonPosix(self):
1467        tz_t1 = tz.tzstr('GMT+3', posix_offset=True)
1468        tz_f1 = tz.tzstr('GMT+3', posix_offset=False)
1469
1470        tz_t2 = tz.tzstr('GMT+3', posix_offset=True)
1471        tz_f2 = tz.tzstr('GMT+3', posix_offset=False)
1472
1473        self.assertIs(tz_t1, tz_t2)
1474        self.assertIsNot(tz_t1, tz_f1)
1475
1476        self.assertIs(tz_f1, tz_f2)
1477
1478    def testTzStrInstance(self):
1479        tz1 = tz.tzstr('EST5EDT')
1480        tz2 = tz.tzstr.instance('EST5EDT')
1481        tz3 = tz.tzstr.instance('EST5EDT')
1482
1483        assert tz1 is not tz2
1484        assert tz2 is not tz3
1485
1486        # Ensure that these still are all the same zone
1487        assert tz1 == tz2 == tz3
1488
1489
1490@pytest.mark.smoke
1491@pytest.mark.tzstr
1492def test_tzstr_weakref():
1493    tz_t1 = tz.tzstr('EST5EDT')
1494    tz_t2_ref = weakref.ref(tz.tzstr('EST5EDT'))
1495    assert tz_t1 is tz_t2_ref()
1496
1497    del tz_t1
1498    gc.collect()
1499
1500    assert tz_t2_ref() is not None
1501    assert tz.tzstr('EST5EDT') is tz_t2_ref()
1502
1503    for offset in range(5,15):
1504        tz.tzstr('GMT+{}'.format(offset))
1505    gc.collect()
1506
1507    assert tz_t2_ref() is None
1508    assert tz.tzstr('EST5EDT') is not tz_t2_ref()
1509
1510
1511@pytest.mark.tzstr
1512@pytest.mark.parametrize('tz_str,expected', [
1513    # From https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html
1514    ('', tz.tzrange(None)),     # TODO: Should change this so tz.tzrange('') works
1515    ('EST+5EDT,M3.2.0/2,M11.1.0/12',
1516     tz.tzrange('EST', -18000, 'EDT', -14400,
1517        start=relativedelta(month=3, day=1, weekday=SU(2), hours=2),
1518        end=relativedelta(month=11, day=1, weekday=SU(1), hours=11))),
1519    ('WART4WARST,J1/0,J365/25',  # This is DST all year, Western Argentina Summer Time
1520     tz.tzrange('WART', timedelta(hours=-4), 'WARST',
1521        start=relativedelta(month=1, day=1, hours=0),
1522        end=relativedelta(month=12, day=31, days=1))),
1523    ('IST-2IDT,M3.4.4/26,M10.5.0',      # Israel Standard / Daylight Time
1524     tz.tzrange('IST', timedelta(hours=2), 'IDT',
1525        start=relativedelta(month=3, day=1, weekday=TH(4), days=1, hours=2),
1526        end=relativedelta(month=10, day=31, weekday=SU(-1), hours=1))),
1527    ('WGT3WGST,M3.5.0/2,M10.5.0/1',
1528     tz.tzrange('WGT', timedelta(hours=-3), 'WGST',
1529        start=relativedelta(month=3, day=31, weekday=SU(-1), hours=2),
1530        end=relativedelta(month=10, day=31, weekday=SU(-1), hours=0))),
1531
1532    # Different offset specifications
1533    ('WGT0300WGST',
1534     tz.tzrange('WGT', timedelta(hours=-3), 'WGST')),
1535    ('WGT03:00WGST',
1536     tz.tzrange('WGT', timedelta(hours=-3), 'WGST')),
1537    ('AEST-1100AEDT',
1538     tz.tzrange('AEST', timedelta(hours=11), 'AEDT')),
1539    ('AEST-11:00AEDT',
1540     tz.tzrange('AEST', timedelta(hours=11), 'AEDT')),
1541
1542    # Different time formats
1543    ('EST5EDT,M3.2.0/4:00,M11.1.0/3:00',
1544     tz.tzrange('EST', timedelta(hours=-5), 'EDT',
1545        start=relativedelta(month=3, day=1, weekday=SU(2), hours=4),
1546        end=relativedelta(month=11, day=1, weekday=SU(1), hours=2))),
1547    ('EST5EDT,M3.2.0/04:00,M11.1.0/03:00',
1548     tz.tzrange('EST', timedelta(hours=-5), 'EDT',
1549        start=relativedelta(month=3, day=1, weekday=SU(2), hours=4),
1550        end=relativedelta(month=11, day=1, weekday=SU(1), hours=2))),
1551    ('EST5EDT,M3.2.0/0400,M11.1.0/0300',
1552     tz.tzrange('EST', timedelta(hours=-5), 'EDT',
1553        start=relativedelta(month=3, day=1, weekday=SU(2), hours=4),
1554        end=relativedelta(month=11, day=1, weekday=SU(1), hours=2))),
1555])
1556def test_valid_GNU_tzstr(tz_str, expected):
1557    tzi = tz.tzstr(tz_str)
1558
1559    assert tzi == expected
1560
1561
1562@pytest.mark.tzstr
1563@pytest.mark.parametrize('tz_str, expected', [
1564    ('EST5EDT,5,4,0,7200,11,3,0,7200',
1565     tz.tzrange('EST', timedelta(hours=-5), 'EDT',
1566        start=relativedelta(month=5, day=1, weekday=SU(+4), hours=+2),
1567        end=relativedelta(month=11, day=1, weekday=SU(+3), hours=+1))),
1568    ('EST5EDT,5,-4,0,7200,11,3,0,7200',
1569     tz.tzrange('EST', timedelta(hours=-5), 'EDT',
1570        start=relativedelta(hours=+2, month=5, day=31, weekday=SU(-4)),
1571        end=relativedelta(hours=+1, month=11, day=1, weekday=SU(+3)))),
1572    ('EST5EDT,5,4,0,7200,11,-3,0,7200',
1573     tz.tzrange('EST', timedelta(hours=-5), 'EDT',
1574        start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)),
1575        end=relativedelta(hours=+1, month=11, day=31, weekday=SU(-3)))),
1576    ('EST5EDT,5,4,0,7200,11,-3,0,7200,3600',
1577     tz.tzrange('EST', timedelta(hours=-5), 'EDT',
1578        start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)),
1579        end=relativedelta(hours=+1, month=11, day=31, weekday=SU(-3)))),
1580    ('EST5EDT,5,4,0,7200,11,-3,0,7200,3600',
1581     tz.tzrange('EST', timedelta(hours=-5), 'EDT',
1582        start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)),
1583        end=relativedelta(hours=+1, month=11, day=31, weekday=SU(-3)))),
1584    ('EST5EDT,5,4,0,7200,11,-3,0,7200,-3600',
1585     tz.tzrange('EST', timedelta(hours=-5), 'EDT', timedelta(hours=-6),
1586        start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)),
1587        end=relativedelta(hours=+3, month=11, day=31, weekday=SU(-3)))),
1588    ('EST5EDT,5,4,0,7200,11,-3,0,7200,+7200',
1589     tz.tzrange('EST', timedelta(hours=-5), 'EDT', timedelta(hours=-3),
1590        start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)),
1591        end=relativedelta(hours=0, month=11, day=31, weekday=SU(-3)))),
1592    ('EST5EDT,5,4,0,7200,11,-3,0,7200,+3600',
1593     tz.tzrange('EST', timedelta(hours=-5), 'EDT',
1594        start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)),
1595        end=relativedelta(hours=+1, month=11, day=31, weekday=SU(-3)))),
1596])
1597def test_valid_dateutil_format(tz_str, expected):
1598    # This tests the dateutil-specific format that is used widely in the tests
1599    # and examples. It is unclear where this format originated from.
1600    with pytest.warns(tz.DeprecatedTzFormatWarning):
1601        tzi = tz.tzstr.instance(tz_str)
1602
1603    assert tzi == expected
1604
1605
1606@pytest.mark.tzstr
1607@pytest.mark.parametrize('tz_str', [
1608    'hdfiughdfuig,dfughdfuigpu87ñ::',
1609    ',dfughdfuigpu87ñ::',
1610    '-1:WART4WARST,J1,J365/25',
1611    'WART4WARST,J1,J365/-25',
1612    'IST-2IDT,M3.4.-1/26,M10.5.0',
1613    'IST-2IDT,M3,2000,1/26,M10,5,0'
1614])
1615def test_invalid_GNU_tzstr(tz_str):
1616    with pytest.raises(ValueError):
1617        tz.tzstr(tz_str)
1618
1619
1620# Different representations of the same default rule set
1621DEFAULT_TZSTR_RULES_EQUIV_2003 = [
1622    'EST5EDT',
1623    'EST5EDT4,M4.1.0/02:00:00,M10-5-0/02:00',
1624    'EST5EDT4,95/02:00:00,298/02:00',
1625    'EST5EDT4,J96/02:00:00,J299/02:00',
1626    'EST5EDT4,J96/02:00:00,J299/02'
1627]
1628
1629
1630@pytest.mark.tzstr
1631@pytest.mark.parametrize('tz_str', DEFAULT_TZSTR_RULES_EQUIV_2003)
1632def test_tzstr_default_start(tz_str):
1633    tzi = tz.tzstr(tz_str)
1634    dt_std = datetime(2003, 4, 6, 1, 59, tzinfo=tzi)
1635    dt_dst = datetime(2003, 4, 6, 2, 00, tzinfo=tzi)
1636
1637    assert get_timezone_tuple(dt_std) == EST_TUPLE
1638    assert get_timezone_tuple(dt_dst) == EDT_TUPLE
1639
1640
1641@pytest.mark.tzstr
1642@pytest.mark.parametrize('tz_str', DEFAULT_TZSTR_RULES_EQUIV_2003)
1643def test_tzstr_default_end(tz_str):
1644    tzi = tz.tzstr(tz_str)
1645    dt_dst = datetime(2003, 10, 26, 0, 59, tzinfo=tzi)
1646    dt_dst_ambig = datetime(2003, 10, 26, 1, 00, tzinfo=tzi)
1647    dt_std_ambig = tz.enfold(dt_dst_ambig, fold=1)
1648    dt_std = datetime(2003, 10, 26, 2, 00, tzinfo=tzi)
1649
1650    assert get_timezone_tuple(dt_dst) == EDT_TUPLE
1651    assert get_timezone_tuple(dt_dst_ambig) == EDT_TUPLE
1652    assert get_timezone_tuple(dt_std_ambig) == EST_TUPLE
1653    assert get_timezone_tuple(dt_std) == EST_TUPLE
1654
1655
1656@pytest.mark.tzstr
1657@pytest.mark.parametrize('tzstr_1', ['EST5EDT',
1658                                     'EST5EDT4,M4.1.0/02:00:00,M10-5-0/02:00'])
1659@pytest.mark.parametrize('tzstr_2', ['EST5EDT',
1660                                     'EST5EDT4,M4.1.0/02:00:00,M10-5-0/02:00'])
1661def test_tzstr_default_cmp(tzstr_1, tzstr_2):
1662    tz1 = tz.tzstr(tzstr_1)
1663    tz2 = tz.tzstr(tzstr_2)
1664
1665    assert tz1 == tz2
1666
1667class TZICalTest(unittest.TestCase, TzFoldMixin):
1668    def _gettz_str_tuple(self, tzname):
1669        TZ_EST = (
1670            'BEGIN:VTIMEZONE',
1671            'TZID:US-Eastern',
1672            'BEGIN:STANDARD',
1673            'DTSTART:19971029T020000',
1674            'RRULE:FREQ=YEARLY;BYDAY=+1SU;BYMONTH=11',
1675            'TZOFFSETFROM:-0400',
1676            'TZOFFSETTO:-0500',
1677            'TZNAME:EST',
1678            'END:STANDARD',
1679            'BEGIN:DAYLIGHT',
1680            'DTSTART:19980301T020000',
1681            'RRULE:FREQ=YEARLY;BYDAY=+2SU;BYMONTH=03',
1682            'TZOFFSETFROM:-0500',
1683            'TZOFFSETTO:-0400',
1684            'TZNAME:EDT',
1685            'END:DAYLIGHT',
1686            'END:VTIMEZONE'
1687        )
1688
1689        TZ_PST = (
1690            'BEGIN:VTIMEZONE',
1691            'TZID:US-Pacific',
1692            'BEGIN:STANDARD',
1693            'DTSTART:19971029T020000',
1694            'RRULE:FREQ=YEARLY;BYDAY=+1SU;BYMONTH=11',
1695            'TZOFFSETFROM:-0700',
1696            'TZOFFSETTO:-0800',
1697            'TZNAME:PST',
1698            'END:STANDARD',
1699            'BEGIN:DAYLIGHT',
1700            'DTSTART:19980301T020000',
1701            'RRULE:FREQ=YEARLY;BYDAY=+2SU;BYMONTH=03',
1702            'TZOFFSETFROM:-0800',
1703            'TZOFFSETTO:-0700',
1704            'TZNAME:PDT',
1705            'END:DAYLIGHT',
1706            'END:VTIMEZONE'
1707        )
1708
1709        TZ_AEST = (
1710            'BEGIN:VTIMEZONE',
1711            'TZID:Australia-Sydney',
1712            'BEGIN:STANDARD',
1713            'DTSTART:19980301T030000',
1714            'RRULE:FREQ=YEARLY;BYDAY=+1SU;BYMONTH=04',
1715            'TZOFFSETFROM:+1100',
1716            'TZOFFSETTO:+1000',
1717            'TZNAME:AEST',
1718            'END:STANDARD',
1719            'BEGIN:DAYLIGHT',
1720            'DTSTART:19971029T020000',
1721            'RRULE:FREQ=YEARLY;BYDAY=+1SU;BYMONTH=10',
1722            'TZOFFSETFROM:+1000',
1723            'TZOFFSETTO:+1100',
1724            'TZNAME:AEDT',
1725            'END:DAYLIGHT',
1726            'END:VTIMEZONE'
1727        )
1728
1729        TZ_LON = (
1730            'BEGIN:VTIMEZONE',
1731            'TZID:Europe-London',
1732            'BEGIN:STANDARD',
1733            'DTSTART:19810301T030000',
1734            'RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10;BYHOUR=02',
1735            'TZOFFSETFROM:+0100',
1736            'TZOFFSETTO:+0000',
1737            'TZNAME:GMT',
1738            'END:STANDARD',
1739            'BEGIN:DAYLIGHT',
1740            'DTSTART:19961001T030000',
1741            'RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=03;BYHOUR=01',
1742            'TZOFFSETFROM:+0000',
1743            'TZOFFSETTO:+0100',
1744            'TZNAME:BST',
1745            'END:DAYLIGHT',
1746            'END:VTIMEZONE'
1747        )
1748
1749        tzname_map = {'Australia/Sydney': TZ_AEST,
1750                      'America/Toronto': TZ_EST,
1751                      'America/New_York': TZ_EST,
1752                      'America/Los_Angeles': TZ_PST,
1753                      'Europe/London': TZ_LON}
1754
1755        return tzname_map[tzname]
1756
1757    def _gettz_str(self, tzname):
1758        return '\n'.join(self._gettz_str_tuple(tzname))
1759
1760    def _tzstr_dtstart_with_params(self, tzname, param_str):
1761        # Adds parameters to the DTSTART values of a given tzstr
1762        tz_str_tuple = self._gettz_str_tuple(tzname)
1763
1764        out_tz = []
1765        for line in tz_str_tuple:
1766            if line.startswith('DTSTART'):
1767                name, value = line.split(':', 1)
1768                line = name + ';' + param_str + ':' + value
1769
1770            out_tz.append(line)
1771
1772        return '\n'.join(out_tz)
1773
1774    def gettz(self, tzname):
1775        tz_str = self._gettz_str(tzname)
1776
1777        tzc = tz.tzical(StringIO(tz_str)).get()
1778
1779        return tzc
1780
1781    def testRepr(self):
1782        instr = StringIO(TZICAL_PST8PDT)
1783        instr.name = 'StringIO(PST8PDT)'
1784        tzc = tz.tzical(instr)
1785
1786        self.assertEqual(repr(tzc), "tzical(" + repr(instr.name) + ")")
1787
1788    # Test performance
1789    def _test_us_zone(self, tzc, func, values, start):
1790        if start:
1791            dt1 = datetime(2003, 3, 9, 1, 59)
1792            dt2 = datetime(2003, 3, 9, 2, 00)
1793            fold = [0, 0]
1794        else:
1795            dt1 = datetime(2003, 11, 2, 0, 59)
1796            dt2 = datetime(2003, 11, 2, 1, 00)
1797            fold = [0, 1]
1798
1799        dts = (tz.enfold(dt.replace(tzinfo=tzc), fold=f)
1800               for dt, f in zip((dt1, dt2), fold))
1801
1802        for value, dt in zip(values, dts):
1803            self.assertEqual(func(dt), value)
1804
1805    def _test_multi_zones(self, tzstrs, tzids, func, values, start):
1806        tzic = tz.tzical(StringIO('\n'.join(tzstrs)))
1807        for tzid, vals in zip(tzids, values):
1808            tzc = tzic.get(tzid)
1809
1810            self._test_us_zone(tzc, func, vals, start)
1811
1812    def _prepare_EST(self):
1813        tz_str = self._gettz_str('America/New_York')
1814        return tz.tzical(StringIO(tz_str)).get()
1815
1816    def _testEST(self, start, test_type, tzc=None):
1817        if tzc is None:
1818            tzc = self._prepare_EST()
1819
1820        argdict = {
1821            'name':   (datetime.tzname, ('EST', 'EDT')),
1822            'offset': (datetime.utcoffset, (timedelta(hours=-5),
1823                                            timedelta(hours=-4))),
1824            'dst':    (datetime.dst, (timedelta(hours=0),
1825                                      timedelta(hours=1)))
1826        }
1827
1828        func, values = argdict[test_type]
1829
1830        if not start:
1831            values = reversed(values)
1832
1833        self._test_us_zone(tzc, func, values, start=start)
1834
1835    def testESTStartName(self):
1836        self._testEST(start=True, test_type='name')
1837
1838    def testESTEndName(self):
1839        self._testEST(start=False, test_type='name')
1840
1841    def testESTStartOffset(self):
1842        self._testEST(start=True, test_type='offset')
1843
1844    def testESTEndOffset(self):
1845        self._testEST(start=False, test_type='offset')
1846
1847    def testESTStartDST(self):
1848        self._testEST(start=True, test_type='dst')
1849
1850    def testESTEndDST(self):
1851        self._testEST(start=False, test_type='dst')
1852
1853    def testESTValueDatetime(self):
1854        # Violating one-test-per-test rule because we're not set up to do
1855        # parameterized tests and the manual proliferation is getting a bit
1856        # out of hand.
1857        tz_str = self._tzstr_dtstart_with_params('America/New_York',
1858                                                 'VALUE=DATE-TIME')
1859
1860        tzc = tz.tzical(StringIO(tz_str)).get()
1861
1862        for start in (True, False):
1863            for test_type in ('name', 'offset', 'dst'):
1864                self._testEST(start=start, test_type=test_type, tzc=tzc)
1865
1866    def _testMultizone(self, start, test_type):
1867        tzstrs = (self._gettz_str('America/New_York'),
1868                  self._gettz_str('America/Los_Angeles'))
1869        tzids = ('US-Eastern', 'US-Pacific')
1870
1871        argdict = {
1872            'name':   (datetime.tzname, (('EST', 'EDT'),
1873                                         ('PST', 'PDT'))),
1874            'offset': (datetime.utcoffset, ((timedelta(hours=-5),
1875                                             timedelta(hours=-4)),
1876                                            (timedelta(hours=-8),
1877                                             timedelta(hours=-7)))),
1878            'dst':    (datetime.dst, ((timedelta(hours=0),
1879                                       timedelta(hours=1)),
1880                                      (timedelta(hours=0),
1881                                       timedelta(hours=1))))
1882        }
1883
1884        func, values = argdict[test_type]
1885
1886        if not start:
1887            values = map(reversed, values)
1888
1889        self._test_multi_zones(tzstrs, tzids, func, values, start)
1890
1891    def testMultiZoneStartName(self):
1892        self._testMultizone(start=True, test_type='name')
1893
1894    def testMultiZoneEndName(self):
1895        self._testMultizone(start=False, test_type='name')
1896
1897    def testMultiZoneStartOffset(self):
1898        self._testMultizone(start=True, test_type='offset')
1899
1900    def testMultiZoneEndOffset(self):
1901        self._testMultizone(start=False, test_type='offset')
1902
1903    def testMultiZoneStartDST(self):
1904        self._testMultizone(start=True, test_type='dst')
1905
1906    def testMultiZoneEndDST(self):
1907        self._testMultizone(start=False, test_type='dst')
1908
1909    def testMultiZoneKeys(self):
1910        est_str = self._gettz_str('America/New_York')
1911        pst_str = self._gettz_str('America/Los_Angeles')
1912        tzic = tz.tzical(StringIO('\n'.join((est_str, pst_str))))
1913
1914        # Sort keys because they are in a random order, being dictionary keys
1915        keys = sorted(tzic.keys())
1916
1917        self.assertEqual(keys, ['US-Eastern', 'US-Pacific'])
1918
1919    # Test error conditions
1920    def testEmptyString(self):
1921        with self.assertRaises(ValueError):
1922            tz.tzical(StringIO(""))
1923
1924    def testMultiZoneGet(self):
1925        tzic = tz.tzical(StringIO(TZICAL_EST5EDT + TZICAL_PST8PDT))
1926
1927        with self.assertRaises(ValueError):
1928            tzic.get()
1929
1930    def testDtstartDate(self):
1931        tz_str = self._tzstr_dtstart_with_params('America/New_York',
1932                                                 'VALUE=DATE')
1933        with self.assertRaises(ValueError):
1934            tz.tzical(StringIO(tz_str))
1935
1936    def testDtstartTzid(self):
1937        tz_str = self._tzstr_dtstart_with_params('America/New_York',
1938                                                 'TZID=UTC')
1939        with self.assertRaises(ValueError):
1940            tz.tzical(StringIO(tz_str))
1941
1942    def testDtstartBadParam(self):
1943        tz_str = self._tzstr_dtstart_with_params('America/New_York',
1944                                                 'FOO=BAR')
1945        with self.assertRaises(ValueError):
1946            tz.tzical(StringIO(tz_str))
1947
1948    # Test Parsing
1949    def testGap(self):
1950        tzic = tz.tzical(StringIO('\n'.join((TZICAL_EST5EDT, TZICAL_PST8PDT))))
1951
1952        keys = sorted(tzic.keys())
1953        self.assertEqual(keys, ['US-Eastern', 'US-Pacific'])
1954
1955
1956class TZTest(unittest.TestCase):
1957    def testFileStart1(self):
1958        tzc = tz.tzfile(BytesIO(base64.b64decode(TZFILE_EST5EDT)))
1959        self.assertEqual(datetime(2003, 4, 6, 1, 59, tzinfo=tzc).tzname(), "EST")
1960        self.assertEqual(datetime(2003, 4, 6, 2, 00, tzinfo=tzc).tzname(), "EDT")
1961
1962    def testFileEnd1(self):
1963        tzc = tz.tzfile(BytesIO(base64.b64decode(TZFILE_EST5EDT)))
1964        self.assertEqual(datetime(2003, 10, 26, 0, 59, tzinfo=tzc).tzname(),
1965                         "EDT")
1966        end_est = tz.enfold(datetime(2003, 10, 26, 1, 00, tzinfo=tzc))
1967        self.assertEqual(end_est.tzname(), "EST")
1968
1969    def testFileLastTransition(self):
1970        # After the last transition, it goes to standard time in perpetuity
1971        tzc = tz.tzfile(BytesIO(base64.b64decode(TZFILE_EST5EDT)))
1972        self.assertEqual(datetime(2037, 10, 25, 0, 59, tzinfo=tzc).tzname(),
1973                         "EDT")
1974
1975        last_date = tz.enfold(datetime(2037, 10, 25, 1, 00, tzinfo=tzc), fold=1)
1976        self.assertEqual(last_date.tzname(),
1977                         "EST")
1978
1979        self.assertEqual(datetime(2038, 5, 25, 12, 0, tzinfo=tzc).tzname(),
1980                         "EST")
1981
1982    def testInvalidFile(self):
1983        # Should throw a ValueError if an invalid file is passed
1984        with self.assertRaises(ValueError):
1985            tz.tzfile(BytesIO(b'BadFile'))
1986
1987    def testFilestreamWithNameRepr(self):
1988        # If fileobj is a filestream with a "name" attribute this name should
1989        # be reflected in the tz object's repr
1990        fileobj = BytesIO(base64.b64decode(TZFILE_EST5EDT))
1991        fileobj.name = 'foo'
1992        tzc = tz.tzfile(fileobj)
1993        self.assertEqual(repr(tzc), 'tzfile(' + repr('foo') + ')')
1994
1995    def testLeapCountDecodesProperly(self):
1996        # This timezone has leapcnt, and failed to decode until
1997        # Eugene Oden notified about the issue.
1998
1999        # As leap information is currently unused (and unstored) by tzfile() we
2000        # can only indirectly test this: Take advantage of tzfile() not closing
2001        # the input file if handed in as an opened file and assert that the
2002        # full file content has been read by tzfile(). Note: For this test to
2003        # work NEW_YORK must be in TZif version 1 format i.e. no more data
2004        # after TZif v1 header + data has been read
2005        fileobj = BytesIO(base64.b64decode(NEW_YORK))
2006        tz.tzfile(fileobj)
2007        # we expect no remaining file content now, i.e. zero-length; if there's
2008        # still data we haven't read the file format correctly
2009        remaining_tzfile_content = fileobj.read()
2010        self.assertEqual(len(remaining_tzfile_content), 0)
2011
2012    def testIsStd(self):
2013        # NEW_YORK tzfile contains this isstd information:
2014        isstd_expected = (0, 0, 0, 1)
2015        tzc = tz.tzfile(BytesIO(base64.b64decode(NEW_YORK)))
2016        # gather the actual information as parsed by the tzfile class
2017        isstd = []
2018        for ttinfo in tzc._ttinfo_list:
2019            # ttinfo objects contain boolean values
2020            isstd.append(int(ttinfo.isstd))
2021        # ttinfo list may contain more entries than isstd file content
2022        isstd = tuple(isstd[:len(isstd_expected)])
2023        self.assertEqual(
2024            isstd_expected, isstd,
2025            "isstd UTC/local indicators parsed: %s != tzfile contents: %s"
2026            % (isstd, isstd_expected))
2027
2028    def testGMTHasNoDaylight(self):
2029        # tz.tzstr("GMT+2") improperly considered daylight saving time.
2030        # Issue reported by Lennart Regebro.
2031        dt = datetime(2007, 8, 6, 4, 10)
2032        self.assertEqual(tz.gettz("GMT+2").dst(dt), timedelta(0))
2033
2034    def testGMTOffset(self):
2035        # GMT and UTC offsets have inverted signal when compared to the
2036        # usual TZ variable handling.
2037        dt = datetime(2007, 8, 6, 4, 10, tzinfo=tz.tzutc())
2038        self.assertEqual(dt.astimezone(tz=tz.tzstr("GMT+2")),
2039                          datetime(2007, 8, 6, 6, 10, tzinfo=tz.tzstr("GMT+2")))
2040        self.assertEqual(dt.astimezone(tz=tz.gettz("UTC-2")),
2041                          datetime(2007, 8, 6, 2, 10, tzinfo=tz.tzstr("UTC-2")))
2042
2043    @unittest.skipIf(IS_WIN, "requires Unix")
2044    @unittest.skipUnless(TZEnvContext.tz_change_allowed(),
2045                         TZEnvContext.tz_change_disallowed_message())
2046    def testTZSetDoesntCorrupt(self):
2047        # if we start in non-UTC then tzset UTC make sure parse doesn't get
2048        # confused
2049        with TZEnvContext('UTC'):
2050            # this should parse to UTC timezone not the original timezone
2051            dt = parse('2014-07-20T12:34:56+00:00')
2052            self.assertEqual(str(dt), '2014-07-20 12:34:56+00:00')
2053
2054
2055@pytest.mark.tzfile
2056@pytest.mark.skipif(not SUPPORTS_SUB_MINUTE_OFFSETS,
2057                    reason='Sub-minute offsets not supported')
2058def test_tzfile_sub_minute_offset():
2059    # If user running python 3.6 or newer, exact offset is used
2060    tzc = tz.tzfile(BytesIO(base64.b64decode(EUROPE_HELSINKI)))
2061    offset = timedelta(hours=1, minutes=39, seconds=52)
2062    assert datetime(1900, 1, 1, 0, 0, tzinfo=tzc).utcoffset() == offset
2063
2064
2065@pytest.mark.tzfile
2066@pytest.mark.skipif(SUPPORTS_SUB_MINUTE_OFFSETS,
2067                    reason='Sub-minute offsets supported.')
2068def test_sub_minute_rounding_tzfile():
2069    # This timezone has an offset of 5992 seconds in 1900-01-01.
2070    # For python version pre-3.6, this will be rounded
2071    tzc = tz.tzfile(BytesIO(base64.b64decode(EUROPE_HELSINKI)))
2072    offset = timedelta(hours=1, minutes=40)
2073    assert datetime(1900, 1, 1, 0, 0, tzinfo=tzc).utcoffset() == offset
2074
2075
2076@pytest.mark.tzfile
2077def test_samoa_transition():
2078    # utcoffset() was erroneously returning +14:00 an hour early (GH #812)
2079    APIA = tz.gettz('Pacific/Apia')
2080    dt = datetime(2011, 12, 29, 23, 59, tzinfo=APIA)
2081    assert dt.utcoffset() == timedelta(hours=-10)
2082
2083    # Make sure the transition actually works, too
2084    dt_after = (dt.astimezone(tz.UTC) + timedelta(minutes=1)).astimezone(APIA)
2085    assert dt_after == datetime(2011, 12, 31, tzinfo=APIA)
2086    assert dt_after.utcoffset() == timedelta(hours=14)
2087
2088
2089@unittest.skipUnless(IS_WIN, "Requires Windows")
2090class TzWinTest(unittest.TestCase, TzWinFoldMixin):
2091    def setUp(self):
2092        self.tzclass = tzwin.tzwin
2093
2094    def testTzResLoadName(self):
2095        # This may not work right on non-US locales.
2096        tzr = tzwin.tzres()
2097        self.assertEqual(tzr.load_name(112), "Eastern Standard Time")
2098
2099    def testTzResNameFromString(self):
2100        tzr = tzwin.tzres()
2101        self.assertEqual(tzr.name_from_string('@tzres.dll,-221'),
2102                         'Alaskan Daylight Time')
2103
2104        self.assertEqual(tzr.name_from_string('Samoa Daylight Time'),
2105                         'Samoa Daylight Time')
2106
2107        with self.assertRaises(ValueError):
2108            tzr.name_from_string('@tzres.dll,100')
2109
2110    def testIsdstZoneWithNoDaylightSaving(self):
2111        tz = tzwin.tzwin("UTC")
2112        dt = parse("2013-03-06 19:08:15")
2113        self.assertFalse(tz._isdst(dt))
2114
2115    def testOffset(self):
2116        tz = tzwin.tzwin("Cape Verde Standard Time")
2117        self.assertEqual(tz.utcoffset(datetime(1995, 5, 21, 12, 9, 13)),
2118                         timedelta(-1, 82800))
2119
2120    def testTzwinName(self):
2121        # https://github.com/dateutil/dateutil/issues/143
2122        tw = tz.tzwin('Eastern Standard Time')
2123
2124        # Cover the transitions for at least two years.
2125        ESTs = 'Eastern Standard Time'
2126        EDTs = 'Eastern Daylight Time'
2127        transition_dates = [(datetime(2015, 3, 8, 0, 59), ESTs),
2128                            (datetime(2015, 3, 8, 3, 1), EDTs),
2129                            (datetime(2015, 11, 1, 0, 59), EDTs),
2130                            (datetime(2015, 11, 1, 3, 1), ESTs),
2131                            (datetime(2016, 3, 13, 0, 59), ESTs),
2132                            (datetime(2016, 3, 13, 3, 1), EDTs),
2133                            (datetime(2016, 11, 6, 0, 59), EDTs),
2134                            (datetime(2016, 11, 6, 3, 1), ESTs)]
2135
2136        for t_date, expected in transition_dates:
2137            self.assertEqual(t_date.replace(tzinfo=tw).tzname(), expected)
2138
2139    def testTzwinRepr(self):
2140        tw = tz.tzwin('Yakutsk Standard Time')
2141        self.assertEqual(repr(tw), 'tzwin(' +
2142                                   repr('Yakutsk Standard Time') + ')')
2143
2144    def testTzWinEquality(self):
2145        # https://github.com/dateutil/dateutil/issues/151
2146        tzwin_names = ('Eastern Standard Time',
2147                       'West Pacific Standard Time',
2148                       'Yakutsk Standard Time',
2149                       'Iran Standard Time',
2150                       'UTC')
2151
2152        for tzwin_name in tzwin_names:
2153            # Get two different instances to compare
2154            tw1 = tz.tzwin(tzwin_name)
2155            tw2 = tz.tzwin(tzwin_name)
2156
2157            self.assertEqual(tw1, tw2)
2158
2159    def testTzWinInequality(self):
2160        # https://github.com/dateutil/dateutil/issues/151
2161        # Note these last two currently differ only in their name.
2162        tzwin_names = (('Eastern Standard Time', 'Yakutsk Standard Time'),
2163                       ('Greenwich Standard Time', 'GMT Standard Time'),
2164                       ('GMT Standard Time', 'UTC'),
2165                       ('E. South America Standard Time',
2166                        'Argentina Standard Time'))
2167
2168        for tzwn1, tzwn2 in tzwin_names:
2169            # Get two different instances to compare
2170            tw1 = tz.tzwin(tzwn1)
2171            tw2 = tz.tzwin(tzwn2)
2172
2173            self.assertNotEqual(tw1, tw2)
2174
2175    def testTzWinEqualityInvalid(self):
2176        # Compare to objects that do not implement comparison with this
2177        # (should default to False)
2178        UTC = tz.tzutc()
2179        EST = tz.tzwin('Eastern Standard Time')
2180
2181        self.assertFalse(EST == UTC)
2182        self.assertFalse(EST == 1)
2183        self.assertFalse(UTC == EST)
2184
2185        self.assertTrue(EST != UTC)
2186        self.assertTrue(EST != 1)
2187
2188    def testTzWinInequalityUnsupported(self):
2189        # Compare it to an object that is promiscuous about equality, but for
2190        # which tzwin does not implement an equality operator.
2191        EST = tz.tzwin('Eastern Standard Time')
2192        self.assertTrue(EST == ComparesEqual)
2193        self.assertFalse(EST != ComparesEqual)
2194
2195    def testTzwinTimeOnlyDST(self):
2196        # For zones with DST, .dst() should return None
2197        tw_est = tz.tzwin('Eastern Standard Time')
2198        self.assertIs(dt_time(14, 10, tzinfo=tw_est).dst(), None)
2199
2200        # This zone has no DST, so .dst() can return 0
2201        tw_sast = tz.tzwin('South Africa Standard Time')
2202        self.assertEqual(dt_time(14, 10, tzinfo=tw_sast).dst(),
2203                         timedelta(0))
2204
2205    def testTzwinTimeOnlyUTCOffset(self):
2206        # For zones with DST, .utcoffset() should return None
2207        tw_est = tz.tzwin('Eastern Standard Time')
2208        self.assertIs(dt_time(14, 10, tzinfo=tw_est).utcoffset(), None)
2209
2210        # This zone has no DST, so .utcoffset() returns standard offset
2211        tw_sast = tz.tzwin('South Africa Standard Time')
2212        self.assertEqual(dt_time(14, 10, tzinfo=tw_sast).utcoffset(),
2213                         timedelta(hours=2))
2214
2215    def testTzwinTimeOnlyTZName(self):
2216        # For zones with DST, the name defaults to standard time
2217        tw_est = tz.tzwin('Eastern Standard Time')
2218        self.assertEqual(dt_time(14, 10, tzinfo=tw_est).tzname(),
2219                         'Eastern Standard Time')
2220
2221        # For zones with no DST, this should work normally.
2222        tw_sast = tz.tzwin('South Africa Standard Time')
2223        self.assertEqual(dt_time(14, 10, tzinfo=tw_sast).tzname(),
2224                         'South Africa Standard Time')
2225
2226
2227@unittest.skipUnless(IS_WIN, "Requires Windows")
2228@unittest.skipUnless(TZWinContext.tz_change_allowed(),
2229                     TZWinContext.tz_change_disallowed_message())
2230class TzWinLocalTest(unittest.TestCase, TzWinFoldMixin):
2231
2232    def setUp(self):
2233        self.tzclass = tzwin.tzwinlocal
2234        self.context = TZWinContext
2235
2236    def get_args(self, tzname):
2237        return ()
2238
2239    def testLocal(self):
2240        # Not sure how to pin a local time zone, so for now we're just going
2241        # to run this and make sure it doesn't raise an error
2242        # See Github Issue #135: https://github.com/dateutil/dateutil/issues/135
2243        datetime.now(tzwin.tzwinlocal())
2244
2245    def testTzwinLocalUTCOffset(self):
2246        with TZWinContext('Eastern Standard Time'):
2247            tzwl = tzwin.tzwinlocal()
2248            self.assertEqual(datetime(2014, 3, 11, tzinfo=tzwl).utcoffset(),
2249                             timedelta(hours=-4))
2250
2251    def testTzwinLocalName(self):
2252        # https://github.com/dateutil/dateutil/issues/143
2253        ESTs = 'Eastern Standard Time'
2254        EDTs = 'Eastern Daylight Time'
2255        transition_dates = [(datetime(2015, 3, 8, 0, 59), ESTs),
2256                            (datetime(2015, 3, 8, 3, 1), EDTs),
2257                            (datetime(2015, 11, 1, 0, 59), EDTs),
2258                            (datetime(2015, 11, 1, 3, 1), ESTs),
2259                            (datetime(2016, 3, 13, 0, 59), ESTs),
2260                            (datetime(2016, 3, 13, 3, 1), EDTs),
2261                            (datetime(2016, 11, 6, 0, 59), EDTs),
2262                            (datetime(2016, 11, 6, 3, 1), ESTs)]
2263
2264        with TZWinContext('Eastern Standard Time'):
2265            tw = tz.tzwinlocal()
2266
2267            for t_date, expected in transition_dates:
2268                self.assertEqual(t_date.replace(tzinfo=tw).tzname(), expected)
2269
2270    def testTzWinLocalRepr(self):
2271        tw = tz.tzwinlocal()
2272        self.assertEqual(repr(tw), 'tzwinlocal()')
2273
2274    def testTzwinLocalRepr(self):
2275        # https://github.com/dateutil/dateutil/issues/143
2276        with TZWinContext('Eastern Standard Time'):
2277            tw = tz.tzwinlocal()
2278
2279            self.assertEqual(str(tw), 'tzwinlocal(' +
2280                                      repr('Eastern Standard Time') + ')')
2281
2282        with TZWinContext('Pacific Standard Time'):
2283            tw = tz.tzwinlocal()
2284
2285            self.assertEqual(str(tw), 'tzwinlocal(' +
2286                                      repr('Pacific Standard Time') + ')')
2287
2288    def testTzwinLocalEquality(self):
2289        tw_est = tz.tzwin('Eastern Standard Time')
2290        tw_pst = tz.tzwin('Pacific Standard Time')
2291
2292        with TZWinContext('Eastern Standard Time'):
2293            twl1 = tz.tzwinlocal()
2294            twl2 = tz.tzwinlocal()
2295
2296            self.assertEqual(twl1, twl2)
2297            self.assertEqual(twl1, tw_est)
2298            self.assertNotEqual(twl1, tw_pst)
2299
2300        with TZWinContext('Pacific Standard Time'):
2301            twl1 = tz.tzwinlocal()
2302            twl2 = tz.tzwinlocal()
2303            tw = tz.tzwin('Pacific Standard Time')
2304
2305            self.assertEqual(twl1, twl2)
2306            self.assertEqual(twl1, tw)
2307            self.assertEqual(twl1, tw_pst)
2308            self.assertNotEqual(twl1, tw_est)
2309
2310    def testTzwinLocalTimeOnlyDST(self):
2311        # For zones with DST, .dst() should return None
2312        with TZWinContext('Eastern Standard Time'):
2313            twl = tz.tzwinlocal()
2314            self.assertIs(dt_time(14, 10, tzinfo=twl).dst(), None)
2315
2316        # This zone has no DST, so .dst() can return 0
2317        with TZWinContext('South Africa Standard Time'):
2318            twl = tz.tzwinlocal()
2319            self.assertEqual(dt_time(14, 10, tzinfo=twl).dst(), timedelta(0))
2320
2321    def testTzwinLocalTimeOnlyUTCOffset(self):
2322        # For zones with DST, .utcoffset() should return None
2323        with TZWinContext('Eastern Standard Time'):
2324            twl = tz.tzwinlocal()
2325            self.assertIs(dt_time(14, 10, tzinfo=twl).utcoffset(), None)
2326
2327        # This zone has no DST, so .utcoffset() returns standard offset
2328        with TZWinContext('South Africa Standard Time'):
2329            twl = tz.tzwinlocal()
2330            self.assertEqual(dt_time(14, 10, tzinfo=twl).utcoffset(),
2331                             timedelta(hours=2))
2332
2333    def testTzwinLocalTimeOnlyTZName(self):
2334        # For zones with DST, the name defaults to standard time
2335        with TZWinContext('Eastern Standard Time'):
2336            twl = tz.tzwinlocal()
2337            self.assertEqual(dt_time(14, 10, tzinfo=twl).tzname(),
2338                             'Eastern Standard Time')
2339
2340        # For zones with no DST, this should work normally.
2341        with TZWinContext('South Africa Standard Time'):
2342            twl = tz.tzwinlocal()
2343            self.assertEqual(dt_time(14, 10, tzinfo=twl).tzname(),
2344                             'South Africa Standard Time')
2345
2346
2347class TzPickleTest(PicklableMixin, unittest.TestCase):
2348    _asfile = False
2349
2350    def setUp(self):
2351        self.assertPicklable = partial(self.assertPicklable,
2352                                       asfile=self._asfile)
2353
2354    def testPickleTzUTC(self):
2355        self.assertPicklable(tz.tzutc(), singleton=True)
2356
2357    def testPickleTzOffsetZero(self):
2358        self.assertPicklable(tz.tzoffset('UTC', 0), singleton=True)
2359
2360    def testPickleTzOffsetPos(self):
2361        self.assertPicklable(tz.tzoffset('UTC+1', 3600), singleton=True)
2362
2363    def testPickleTzOffsetNeg(self):
2364        self.assertPicklable(tz.tzoffset('UTC-1', -3600), singleton=True)
2365
2366    @pytest.mark.tzlocal
2367    def testPickleTzLocal(self):
2368        self.assertPicklable(tz.tzlocal())
2369
2370    def testPickleTzFileEST5EDT(self):
2371        tzc = tz.tzfile(BytesIO(base64.b64decode(TZFILE_EST5EDT)))
2372        self.assertPicklable(tzc)
2373
2374    def testPickleTzFileEurope_Helsinki(self):
2375        tzc = tz.tzfile(BytesIO(base64.b64decode(EUROPE_HELSINKI)))
2376        self.assertPicklable(tzc)
2377
2378    def testPickleTzFileNew_York(self):
2379        tzc = tz.tzfile(BytesIO(base64.b64decode(NEW_YORK)))
2380        self.assertPicklable(tzc)
2381
2382    @unittest.skip("Known failure")
2383    def testPickleTzICal(self):
2384        tzc = tz.tzical(StringIO(TZICAL_EST5EDT)).get()
2385        self.assertPicklable(tzc)
2386
2387    def testPickleTzGettz(self):
2388        self.assertPicklable(tz.gettz('America/New_York'))
2389
2390    def testPickleZoneFileGettz(self):
2391        zoneinfo_file = zoneinfo.get_zonefile_instance()
2392        tzi = zoneinfo_file.get('America/New_York')
2393        self.assertIsNot(tzi, None)
2394        self.assertPicklable(tzi)
2395
2396
2397class TzPickleFileTest(TzPickleTest):
2398    """ Run all the TzPickleTest tests, using a temporary file """
2399    _asfile = True
2400
2401
2402class DatetimeAmbiguousTest(unittest.TestCase):
2403    """ Test the datetime_exists / datetime_ambiguous functions """
2404
2405    def testNoTzSpecified(self):
2406        with self.assertRaises(ValueError):
2407            tz.datetime_ambiguous(datetime(2016, 4, 1, 2, 9))
2408
2409    def _get_no_support_tzinfo_class(self, dt_start, dt_end, dst_only=False):
2410        # Generates a class of tzinfo with no support for is_ambiguous
2411        # where dates between dt_start and dt_end are ambiguous.
2412
2413        class FoldingTzInfo(tzinfo):
2414            def utcoffset(self, dt):
2415                if not dst_only:
2416                    dt_n = dt.replace(tzinfo=None)
2417
2418                    if dt_start <= dt_n < dt_end and getattr(dt_n, 'fold', 0):
2419                        return timedelta(hours=-1)
2420
2421                return timedelta(hours=0)
2422
2423            def dst(self, dt):
2424                dt_n = dt.replace(tzinfo=None)
2425
2426                if dt_start <= dt_n < dt_end and getattr(dt_n, 'fold', 0):
2427                    return timedelta(hours=1)
2428                else:
2429                    return timedelta(0)
2430
2431        return FoldingTzInfo
2432
2433    def _get_no_support_tzinfo(self, dt_start, dt_end, dst_only=False):
2434        return self._get_no_support_tzinfo_class(dt_start, dt_end, dst_only)()
2435
2436    def testNoSupportAmbiguityFoldNaive(self):
2437        dt_start = datetime(2018, 9, 1, 1, 0)
2438        dt_end = datetime(2018, 9, 1, 2, 0)
2439
2440        tzi = self._get_no_support_tzinfo(dt_start, dt_end)
2441
2442        self.assertTrue(tz.datetime_ambiguous(datetime(2018, 9, 1, 1, 30),
2443                                              tz=tzi))
2444
2445    def testNoSupportAmbiguityFoldAware(self):
2446        dt_start = datetime(2018, 9, 1, 1, 0)
2447        dt_end = datetime(2018, 9, 1, 2, 0)
2448
2449        tzi = self._get_no_support_tzinfo(dt_start, dt_end)
2450
2451        self.assertTrue(tz.datetime_ambiguous(datetime(2018, 9, 1, 1, 30,
2452                                                       tzinfo=tzi)))
2453
2454    def testNoSupportAmbiguityUnambiguousNaive(self):
2455        dt_start = datetime(2018, 9, 1, 1, 0)
2456        dt_end = datetime(2018, 9, 1, 2, 0)
2457
2458        tzi = self._get_no_support_tzinfo(dt_start, dt_end)
2459
2460        self.assertFalse(tz.datetime_ambiguous(datetime(2018, 10, 1, 12, 30),
2461                                              tz=tzi))
2462
2463    def testNoSupportAmbiguityUnambiguousAware(self):
2464        dt_start = datetime(2018, 9, 1, 1, 0)
2465        dt_end = datetime(2018, 9, 1, 2, 0)
2466
2467        tzi = self._get_no_support_tzinfo(dt_start, dt_end)
2468
2469        self.assertFalse(tz.datetime_ambiguous(datetime(2018, 10, 1, 12, 30,
2470                                                        tzinfo=tzi)))
2471
2472    def testNoSupportAmbiguityFoldDSTOnly(self):
2473        dt_start = datetime(2018, 9, 1, 1, 0)
2474        dt_end = datetime(2018, 9, 1, 2, 0)
2475
2476        tzi = self._get_no_support_tzinfo(dt_start, dt_end, dst_only=True)
2477
2478        self.assertTrue(tz.datetime_ambiguous(datetime(2018, 9, 1, 1, 30),
2479                                              tz=tzi))
2480
2481    def testNoSupportAmbiguityUnambiguousDSTOnly(self):
2482        dt_start = datetime(2018, 9, 1, 1, 0)
2483        dt_end = datetime(2018, 9, 1, 2, 0)
2484
2485        tzi = self._get_no_support_tzinfo(dt_start, dt_end, dst_only=True)
2486
2487        self.assertFalse(tz.datetime_ambiguous(datetime(2018, 10, 1, 12, 30),
2488                                               tz=tzi))
2489
2490    def testSupportAmbiguityFoldNaive(self):
2491        tzi = tz.gettz('US/Eastern')
2492
2493        dt = datetime(2011, 11, 6, 1, 30)
2494
2495        self.assertTrue(tz.datetime_ambiguous(dt, tz=tzi))
2496
2497    def testSupportAmbiguityFoldAware(self):
2498        tzi = tz.gettz('US/Eastern')
2499
2500        dt = datetime(2011, 11, 6, 1, 30, tzinfo=tzi)
2501
2502        self.assertTrue(tz.datetime_ambiguous(dt))
2503
2504    def testSupportAmbiguityUnambiguousAware(self):
2505        tzi = tz.gettz('US/Eastern')
2506
2507        dt = datetime(2011, 11, 6, 4, 30)
2508
2509        self.assertFalse(tz.datetime_ambiguous(dt, tz=tzi))
2510
2511    def testSupportAmbiguityUnambiguousNaive(self):
2512        tzi = tz.gettz('US/Eastern')
2513
2514        dt = datetime(2011, 11, 6, 4, 30, tzinfo=tzi)
2515
2516        self.assertFalse(tz.datetime_ambiguous(dt))
2517
2518    def _get_ambig_error_tzinfo(self, dt_start, dt_end, dst_only=False):
2519        cTzInfo = self._get_no_support_tzinfo_class(dt_start, dt_end, dst_only)
2520
2521        # Takes the wrong number of arguments and raises an error anyway.
2522        class FoldTzInfoRaises(cTzInfo):
2523            def is_ambiguous(self, dt, other_arg):
2524                raise NotImplementedError('This is not implemented')
2525
2526        return FoldTzInfoRaises()
2527
2528    def testIncompatibleAmbiguityFoldNaive(self):
2529        dt_start = datetime(2018, 9, 1, 1, 0)
2530        dt_end = datetime(2018, 9, 1, 2, 0)
2531
2532        tzi = self._get_ambig_error_tzinfo(dt_start, dt_end)
2533
2534        self.assertTrue(tz.datetime_ambiguous(datetime(2018, 9, 1, 1, 30),
2535                                              tz=tzi))
2536
2537    def testIncompatibleAmbiguityFoldAware(self):
2538        dt_start = datetime(2018, 9, 1, 1, 0)
2539        dt_end = datetime(2018, 9, 1, 2, 0)
2540
2541        tzi = self._get_ambig_error_tzinfo(dt_start, dt_end)
2542
2543        self.assertTrue(tz.datetime_ambiguous(datetime(2018, 9, 1, 1, 30,
2544                                                       tzinfo=tzi)))
2545
2546    def testIncompatibleAmbiguityUnambiguousNaive(self):
2547        dt_start = datetime(2018, 9, 1, 1, 0)
2548        dt_end = datetime(2018, 9, 1, 2, 0)
2549
2550        tzi = self._get_ambig_error_tzinfo(dt_start, dt_end)
2551
2552        self.assertFalse(tz.datetime_ambiguous(datetime(2018, 10, 1, 12, 30),
2553                                              tz=tzi))
2554
2555    def testIncompatibleAmbiguityUnambiguousAware(self):
2556        dt_start = datetime(2018, 9, 1, 1, 0)
2557        dt_end = datetime(2018, 9, 1, 2, 0)
2558
2559        tzi = self._get_ambig_error_tzinfo(dt_start, dt_end)
2560
2561        self.assertFalse(tz.datetime_ambiguous(datetime(2018, 10, 1, 12, 30,
2562                                                        tzinfo=tzi)))
2563
2564    def testIncompatibleAmbiguityFoldDSTOnly(self):
2565        dt_start = datetime(2018, 9, 1, 1, 0)
2566        dt_end = datetime(2018, 9, 1, 2, 0)
2567
2568        tzi = self._get_ambig_error_tzinfo(dt_start, dt_end, dst_only=True)
2569
2570        self.assertTrue(tz.datetime_ambiguous(datetime(2018, 9, 1, 1, 30),
2571                                              tz=tzi))
2572
2573    def testIncompatibleAmbiguityUnambiguousDSTOnly(self):
2574        dt_start = datetime(2018, 9, 1, 1, 0)
2575        dt_end = datetime(2018, 9, 1, 2, 0)
2576
2577        tzi = self._get_ambig_error_tzinfo(dt_start, dt_end, dst_only=True)
2578
2579        self.assertFalse(tz.datetime_ambiguous(datetime(2018, 10, 1, 12, 30),
2580                                               tz=tzi))
2581
2582    def testSpecifiedTzOverridesAttached(self):
2583        # If a tz is specified, the datetime will be treated as naive.
2584
2585        # This is not ambiguous in the local zone
2586        dt = datetime(2011, 11, 6, 1, 30, tzinfo=tz.gettz('Australia/Sydney'))
2587
2588        self.assertFalse(tz.datetime_ambiguous(dt))
2589
2590        tzi = tz.gettz('US/Eastern')
2591        self.assertTrue(tz.datetime_ambiguous(dt, tz=tzi))
2592
2593
2594class DatetimeExistsTest(unittest.TestCase):
2595    def testNoTzSpecified(self):
2596        with self.assertRaises(ValueError):
2597            tz.datetime_exists(datetime(2016, 4, 1, 2, 9))
2598
2599    def testInGapNaive(self):
2600        tzi = tz.gettz('Australia/Sydney')
2601
2602        dt = datetime(2012, 10, 7, 2, 30)
2603
2604        self.assertFalse(tz.datetime_exists(dt, tz=tzi))
2605
2606    def testInGapAware(self):
2607        tzi = tz.gettz('Australia/Sydney')
2608
2609        dt = datetime(2012, 10, 7, 2, 30, tzinfo=tzi)
2610
2611        self.assertFalse(tz.datetime_exists(dt))
2612
2613    def testExistsNaive(self):
2614        tzi = tz.gettz('Australia/Sydney')
2615
2616        dt = datetime(2012, 10, 7, 10, 30)
2617
2618        self.assertTrue(tz.datetime_exists(dt, tz=tzi))
2619
2620    def testExistsAware(self):
2621        tzi = tz.gettz('Australia/Sydney')
2622
2623        dt = datetime(2012, 10, 7, 10, 30, tzinfo=tzi)
2624
2625        self.assertTrue(tz.datetime_exists(dt))
2626
2627    def testSpecifiedTzOverridesAttached(self):
2628        EST = tz.gettz('US/Eastern')
2629        AEST = tz.gettz('Australia/Sydney')
2630
2631        dt = datetime(2012, 10, 7, 2, 30, tzinfo=EST)  # This time exists
2632
2633        self.assertFalse(tz.datetime_exists(dt, tz=AEST))
2634
2635
2636class TestEnfold:
2637    def test_enter_fold_default(self):
2638        dt = tz.enfold(datetime(2020, 1, 19, 3, 32))
2639
2640        assert dt.fold == 1
2641
2642    def test_enter_fold(self):
2643        dt = tz.enfold(datetime(2020, 1, 19, 3, 32), fold=1)
2644
2645        assert dt.fold == 1
2646
2647    def test_exit_fold(self):
2648        dt = tz.enfold(datetime(2020, 1, 19, 3, 32), fold=0)
2649
2650        # Before Python 3.6, dt.fold won't exist if fold is 0.
2651        assert getattr(dt, 'fold', 0) == 0
2652
2653    def test_defold(self):
2654        dt = tz.enfold(datetime(2020, 1, 19, 3, 32), fold=1)
2655
2656        dt2 = tz.enfold(dt, fold=0)
2657
2658        assert getattr(dt2, 'fold', 0) == 0
2659
2660    def test_fold_replace_args(self):
2661        # This test can be dropped when Python < 3.6 is dropped, since it
2662        # is mainly to cover the `replace` method on _DatetimeWithFold
2663        dt = tz.enfold(datetime(1950, 1, 2, 12, 30, 15, 8), fold=1)
2664
2665        dt2 = dt.replace(1952, 2, 3, 13, 31, 16, 9)
2666        assert dt2 == tz.enfold(datetime(1952, 2, 3, 13, 31, 16, 9), fold=1)
2667        assert dt2.fold == 1
2668
2669    def test_fold_replace_exception_duplicate_args(self):
2670        dt = tz.enfold(datetime(1999, 1, 3), fold=1)
2671
2672        with pytest.raises(TypeError):
2673            dt.replace(1950, year=2000)
2674
2675
2676@pytest.mark.tz_resolve_imaginary
2677class ImaginaryDateTest(unittest.TestCase):
2678    def testCanberraForward(self):
2679        tzi = tz.gettz('Australia/Canberra')
2680        dt = datetime(2018, 10, 7, 2, 30, tzinfo=tzi)
2681        dt_act = tz.resolve_imaginary(dt)
2682        dt_exp = datetime(2018, 10, 7, 3, 30, tzinfo=tzi)
2683        self.assertEqual(dt_act, dt_exp)
2684
2685    def testLondonForward(self):
2686        tzi = tz.gettz('Europe/London')
2687        dt = datetime(2018, 3, 25, 1, 30, tzinfo=tzi)
2688        dt_act = tz.resolve_imaginary(dt)
2689        dt_exp = datetime(2018, 3, 25, 2, 30, tzinfo=tzi)
2690        self.assertEqual(dt_act, dt_exp)
2691
2692    def testKeivForward(self):
2693        tzi = tz.gettz('Europe/Kiev')
2694        dt = datetime(2018, 3, 25, 3, 30, tzinfo=tzi)
2695        dt_act = tz.resolve_imaginary(dt)
2696        dt_exp = datetime(2018, 3, 25, 4, 30, tzinfo=tzi)
2697        self.assertEqual(dt_act, dt_exp)
2698
2699
2700@pytest.mark.tz_resolve_imaginary
2701@pytest.mark.parametrize('dt', [
2702    datetime(2017, 11, 5, 1, 30, tzinfo=tz.gettz('America/New_York')),
2703    datetime(2018, 10, 28, 1, 30, tzinfo=tz.gettz('Europe/London')),
2704    datetime(2017, 4, 2, 2, 30, tzinfo=tz.gettz('Australia/Sydney')),
2705])
2706def test_resolve_imaginary_ambiguous(dt):
2707    assert tz.resolve_imaginary(dt) is dt
2708
2709    dt_f = tz.enfold(dt)
2710    assert dt is not dt_f
2711    assert tz.resolve_imaginary(dt_f) is dt_f
2712
2713
2714@pytest.mark.tz_resolve_imaginary
2715@pytest.mark.parametrize('dt', [
2716    datetime(2017, 6, 2, 12, 30, tzinfo=tz.gettz('America/New_York')),
2717    datetime(2018, 4, 2, 9, 30, tzinfo=tz.gettz('Europe/London')),
2718    datetime(2017, 2, 2, 16, 30, tzinfo=tz.gettz('Australia/Sydney')),
2719    datetime(2017, 12, 2, 12, 30, tzinfo=tz.gettz('America/New_York')),
2720    datetime(2018, 12, 2, 9, 30, tzinfo=tz.gettz('Europe/London')),
2721    datetime(2017, 6, 2, 16, 30, tzinfo=tz.gettz('Australia/Sydney')),
2722    datetime(2025, 9, 25, 1, 17, tzinfo=tz.tzutc()),
2723    datetime(2025, 9, 25, 1, 17, tzinfo=tz.tzoffset('EST', -18000)),
2724    datetime(2019, 3, 4, tzinfo=None)
2725])
2726def test_resolve_imaginary_existing(dt):
2727    assert tz.resolve_imaginary(dt) is dt
2728
2729
2730def __get_kiritimati_resolve_imaginary_test():
2731    # In the 2018d release of the IANA database, the Kiritimati "imaginary day"
2732    # data was corrected, so if the system zoneinfo is older than 2018d, the
2733    # Kiritimati test will fail.
2734
2735    tzi = tz.gettz('Pacific/Kiritimati')
2736    new_version = False
2737    if not tz.datetime_exists(datetime(1995, 1, 1, 12, 30), tzi):
2738        zif = zoneinfo.get_zonefile_instance()
2739        if zif.metadata is not None:
2740            new_version = zif.metadata['tzversion'] >= '2018d'
2741
2742        if new_version:
2743            tzi = zif.get('Pacific/Kiritimati')
2744    else:
2745        new_version = True
2746
2747    if new_version:
2748        dates = (datetime(1994, 12, 31, 12, 30), datetime(1995, 1, 1, 12, 30))
2749    else:
2750        dates = (datetime(1995, 1, 1, 12, 30), datetime(1995, 1, 2, 12, 30))
2751
2752    return (tzi, ) + dates
2753
2754
2755resolve_imaginary_tests = [
2756    (tz.gettz('Europe/London'),
2757     datetime(2018, 3, 25, 1, 30), datetime(2018, 3, 25, 2, 30)),
2758    (tz.gettz('America/New_York'),
2759     datetime(2017, 3, 12, 2, 30), datetime(2017, 3, 12, 3, 30)),
2760    (tz.gettz('Australia/Sydney'),
2761     datetime(2014, 10, 5, 2, 0), datetime(2014, 10, 5, 3, 0)),
2762    __get_kiritimati_resolve_imaginary_test(),
2763]
2764
2765
2766if SUPPORTS_SUB_MINUTE_OFFSETS:
2767    resolve_imaginary_tests.append(
2768        (tz.gettz('Africa/Monrovia'),
2769         datetime(1972, 1, 7, 0, 30), datetime(1972, 1, 7, 1, 14, 30)))
2770
2771
2772@pytest.mark.tz_resolve_imaginary
2773@pytest.mark.parametrize('tzi, dt, dt_exp', resolve_imaginary_tests)
2774def test_resolve_imaginary(tzi, dt, dt_exp):
2775    dt = dt.replace(tzinfo=tzi)
2776    dt_exp = dt_exp.replace(tzinfo=tzi)
2777
2778    dt_r = tz.resolve_imaginary(dt)
2779    assert dt_r == dt_exp
2780    assert dt_r.tzname() == dt_exp.tzname()
2781    assert dt_r.utcoffset() == dt_exp.utcoffset()
2782