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

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

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

20 20
from functools import partial
21

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

24 20
import pytest
25

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

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

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

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

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

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

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

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

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

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

161

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

168

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

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

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

181

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

436

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

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

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

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

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

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

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

467 9
        return t_n, t0_u, t1_u
468

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

618 20
        self.assertIs(UTC_0, UTC_1)
619

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

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

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

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

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

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

638 20
        self.assertEqual(UTC0, UTC1)
639

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

644 20
        self.assertNotEqual(UTC, UTCp4)
645

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

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

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

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

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

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

670

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

677 20
        self.assertEqual(est, est_s)
678

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

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

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

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

699 20
        self.assertEqual(utc, gmt)
700

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

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

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

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

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

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

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

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

730 20
        assert tz1 is not tz2
731

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

736 20
        assert tz1 is tz2
737

738

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

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

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

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

759

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

771 20
    assert tz1 is tz2
772

773

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

782

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

791

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

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

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

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

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

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

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

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

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

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

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

835

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

847

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

851

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

918 20
    def testOffsetUTC(self):
919 11
        self._testOffset(self.UTC, timedelta(0), timedelta(0))
920

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

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

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

930 20
    def testDSTUTC(self):
931 11
        self._testDST(self.UTC, timedelta(0))
932

933 20
    def testTimeOnlyOffsetLocalUTC(self):
934 11
        with TZEnvContext(self.UTC):
935 11
            self.assertEqual(dt_time(13, 20, tzinfo=tz.tzlocal()).utcoffset(),
936
                             timedelta(0))
937

938 20
    def testTimeOnlyOffsetLocalDST(self):
939 11
        with TZEnvContext(self.TZ_EST):
940 11
            self.assertIs(dt_time(13, 20, tzinfo=tz.tzlocal()).utcoffset(),
941
                          None)
942

943 20
    def testTimeOnlyDSTLocalUTC(self):
944 11
        with TZEnvContext(self.UTC):
945 11
            self.assertEqual(dt_time(13, 20, tzinfo=tz.tzlocal()).dst(),
946
                             timedelta(0))
947

948 20
    def testTimeOnlyDSTLocalDST(self):
949 11
        with TZEnvContext(self.TZ_EST):
950 11
            self.assertIs(dt_time(13, 20, tzinfo=tz.tzlocal()).dst(),
951
                          None)
952

953 20
    def testUTCEquality(self):
954 11
        with TZEnvContext(self.UTC):
955 11
            assert tz.tzlocal() == tz.UTC
956

957

958
# TODO: Maybe a better hack than this?
959 20
def mark_tzlocal_nix(f):
960 20
    marks = [
961
        pytest.mark.tzlocal,
962
        pytest.mark.skipif(IS_WIN, reason='requires Unix'),
963
    ]
964

965 20
    for mark in reversed(marks):
966 20
        f = mark(f)
967

968 20
    return f
969

970

971 20
@mark_tzlocal_nix
972 20
@pytest.mark.parametrize('tzvar', ['UTC', 'GMT0', 'UTC0'])
973 2
def test_tzlocal_utc_equal(tzvar):
974 11
    with TZEnvContext(tzvar):
975 11
        assert tz.tzlocal() == tz.UTC
976

977

978 20
@mark_tzlocal_nix
979 20
@pytest.mark.parametrize('tzvar', [
980
    'Europe/London', 'America/New_York',
981
    'GMT0BST', 'EST5EDT'])
982 2
def test_tzlocal_utc_unequal(tzvar):
983 11
    with TZEnvContext(tzvar):
984 11
        assert tz.tzlocal() != tz.UTC
985

986

987 20
@mark_tzlocal_nix
988 2
def test_tzlocal_local_time_trim_colon():
989 11
    with TZEnvContext(':/etc/localtime'):
990 11
        assert tz.gettz() is not None
991

992

993 20
@mark_tzlocal_nix
994 20
@pytest.mark.parametrize('tzvar, tzoff', [
995
    ('EST5', tz.tzoffset('EST', -18000)),
996
    ('GMT0', tz.tzoffset('GMT', 0)),
997
    ('YAKT-9', tz.tzoffset('YAKT', timedelta(hours=9))),
998
    ('JST-9', tz.tzoffset('JST', timedelta(hours=9))),
999
])
1000 2
def test_tzlocal_offset_equal(tzvar, tzoff):
1001 11
    with TZEnvContext(tzvar):
1002
        # Including both to test both __eq__ and __ne__
1003 11
        assert tz.tzlocal() == tzoff
1004 11
        assert not (tz.tzlocal() != tzoff)
1005

1006

1007 20
@mark_tzlocal_nix
1008 20
@pytest.mark.parametrize('tzvar, tzoff', [
1009
    ('EST5EDT', tz.tzoffset('EST', -18000)),
1010
    ('GMT0BST', tz.tzoffset('GMT', 0)),
1011
    ('EST5', tz.tzoffset('EST', -14400)),
1012
    ('YAKT-9', tz.tzoffset('JST', timedelta(hours=9))),
1013
    ('JST-9', tz.tzoffset('YAKT', timedelta(hours=9))),
1014
])
1015 2
def test_tzlocal_offset_unequal(tzvar, tzoff):
1016 11
    with TZEnvContext(tzvar):
1017
        # Including both to test both __eq__ and __ne__
1018 11
        assert tz.tzlocal() != tzoff
1019 11
        assert not (tz.tzlocal() == tzoff)
1020

1021

1022 20
@pytest.mark.gettz
1023 20
class GettzTest(unittest.TestCase, TzFoldMixin):
1024 20
    gettz = staticmethod(tz.gettz)
1025

1026 20
    def testGettz(self):
1027
        # bug 892569
1028 20
        str(self.gettz('UTC'))
1029

1030 20
    def testGetTzEquality(self):
1031 20
        self.assertEqual(self.gettz('UTC'), self.gettz('UTC'))
1032

1033 20
    def testTimeOnlyGettz(self):
1034
        # gettz returns None
1035 20
        tz_get = self.gettz('Europe/Minsk')
1036 20
        self.assertIs(dt_time(13, 20, tzinfo=tz_get).utcoffset(), None)
1037

1038 20
    def testTimeOnlyGettzDST(self):
1039
        # gettz returns None
1040 20
        tz_get = self.gettz('Europe/Minsk')
1041 20
        self.assertIs(dt_time(13, 20, tzinfo=tz_get).dst(), None)
1042

1043 20
    def testTimeOnlyGettzTzName(self):
1044 20
        tz_get = self.gettz('Europe/Minsk')
1045 20
        self.assertIs(dt_time(13, 20, tzinfo=tz_get).tzname(), None)
1046

1047 20
    def testTimeOnlyFormatZ(self):
1048 20
        tz_get = self.gettz('Europe/Minsk')
1049 20
        t = dt_time(13, 20, tzinfo=tz_get)
1050

1051 20
        self.assertEqual(t.strftime('%H%M%Z'), '1320')
1052

1053 20
    def testPortugalDST(self):
1054
        # In 1996, Portugal changed from CET to WET
1055 20
        PORTUGAL = self.gettz('Portugal')
1056

1057 20
        t_cet = datetime(1996, 3, 31, 1, 59, tzinfo=PORTUGAL)
1058

1059 20
        self.assertEqual(t_cet.tzname(), 'CET')
1060 20
        self.assertEqual(t_cet.utcoffset(), timedelta(hours=1))
1061 20
        self.assertEqual(t_cet.dst(), timedelta(0))
1062

1063 20
        t_west = datetime(1996, 3, 31, 2, 1, tzinfo=PORTUGAL)
1064

1065 20
        self.assertEqual(t_west.tzname(), 'WEST')
1066 20
        self.assertEqual(t_west.utcoffset(), timedelta(hours=1))
1067 20
        self.assertEqual(t_west.dst(), timedelta(hours=1))
1068

1069 20
    def testGettzCacheTzFile(self):
1070 20
        NYC1 = tz.gettz('America/New_York')
1071 20
        NYC2 = tz.gettz('America/New_York')
1072

1073 20
        assert NYC1 is NYC2
1074

1075 20
    def testGettzCacheTzLocal(self):
1076 20
        local1 = tz.gettz()
1077 20
        local2 = tz.gettz()
1078

1079 20
        assert local1 is not local2
1080

1081

1082 20
@pytest.mark.gettz
1083 2
def test_gettz_same_result_for_none_and_empty_string():
1084 20
    local_from_none = tz.gettz()
1085 20
    local_from_empty_string = tz.gettz("")
1086 20
    assert local_from_none is not None
1087 20
    assert local_from_empty_string is not None
1088 20
    assert local_from_none == local_from_empty_string
1089

1090

1091 20
@pytest.mark.gettz
1092 20
@pytest.mark.parametrize('badzone', [
1093
    'Fake.Region/Abcdefghijklmnop',  # Violates several tz project name rules
1094
])
1095 2
def test_gettz_badzone(badzone):
1096
    # Make sure passing a bad TZ string to gettz returns None (GH #800)
1097 20
    tzi = tz.gettz(badzone)
1098 20
    assert tzi is None
1099

1100

1101 20
@pytest.mark.gettz
1102 2
def test_gettz_badzone_unicode():
1103
    # Make sure a unicode string can be passed to TZ (GH #802)
1104
    # When fixed, combine this with test_gettz_badzone
1105 20
    tzi = tz.gettz('🐼')
1106 20
    assert tzi is None
1107

1108

1109 20
@pytest.mark.gettz
1110 20
@pytest.mark.parametrize(
1111
    "badzone,exc_reason",
1112
    [
1113
        pytest.param(
1114
            b"America/New_York",
1115
            ".*should be str, not bytes.*",
1116
            id="bytes on Python 3",
1117
            marks=[
1118
                pytest.mark.skipif(
1119
                    PY2, reason="bytes arguments accepted in Python 2"
1120
                )
1121
            ],
1122
        ),
1123
        pytest.param(
1124
            object(),
1125
            None,
1126
            id="no startswith()",
1127
            marks=[
1128
                pytest.mark.xfail(reason="AttributeError instead of TypeError",
1129
                                  raises=AttributeError),
1130
            ],
1131
        ),
1132
    ],
1133
)
1134 2
def test_gettz_zone_wrong_type(badzone, exc_reason):
1135 14
    with pytest.raises(TypeError, match=exc_reason):
1136 14
        tz.gettz(badzone)
1137

1138

1139 20
@pytest.mark.gettz
1140 20
@pytest.mark.xfail(IS_WIN, reason='zoneinfo separately cached')
1141 2
def test_gettz_cache_clear():
1142 11
    NYC1 = tz.gettz('America/New_York')
1143 11
    tz.gettz.cache_clear()
1144

1145 11
    NYC2 = tz.gettz('America/New_York')
1146

1147 11
    assert NYC1 is not NYC2
1148

1149 20
@pytest.mark.gettz
1150 20
@pytest.mark.xfail(IS_WIN, reason='zoneinfo separately cached')
1151 2
def test_gettz_set_cache_size():
1152 11
    tz.gettz.cache_clear()
1153 11
    tz.gettz.set_cache_size(3)
1154

1155 11
    MONACO_ref = weakref.ref(tz.gettz('Europe/Monaco'))
1156 11
    EASTER_ref = weakref.ref(tz.gettz('Pacific/Easter'))
1157 11
    CURRIE_ref = weakref.ref(tz.gettz('Australia/Currie'))
1158

1159 11
    gc.collect()
1160

1161 11
    assert MONACO_ref() is not None
1162 11
    assert EASTER_ref() is not None
1163 11
    assert CURRIE_ref() is not None
1164

1165 11
    tz.gettz.set_cache_size(2)
1166 11
    gc.collect()
1167

1168 11
    assert MONACO_ref() is None
1169

1170 20
@pytest.mark.xfail(IS_WIN, reason="Windows does not use system zoneinfo")
1171 20
@pytest.mark.smoke
1172 20
@pytest.mark.gettz
1173 2
def test_gettz_weakref():
1174 11
    tz.gettz.cache_clear()
1175 11
    tz.gettz.set_cache_size(2)
1176 11
    NYC1 = tz.gettz('America/New_York')
1177 11
    NYC_ref = weakref.ref(tz.gettz('America/New_York'))
1178

1179 11
    assert NYC1 is NYC_ref()
1180

1181 11
    del NYC1
1182 11
    gc.collect()
1183

1184 11
    assert NYC_ref() is not None        # Should still be in the strong cache
1185 11
    assert tz.gettz('America/New_York') is NYC_ref()
1186

1187
    # Populate strong cache with other timezones
1188 11
    tz.gettz('Europe/Monaco')
1189 11
    tz.gettz('Pacific/Easter')
1190 11
    tz.gettz('Australia/Currie')
1191

1192 11
    gc.collect()
1193 11
    assert NYC_ref() is None    # Should have been pushed out
1194 11
    assert tz.gettz('America/New_York') is not NYC_ref()
1195

1196 20
class ZoneInfoGettzTest(GettzTest):
1197 20
    def gettz(self, name):
1198 20
        zoneinfo_file = zoneinfo.get_zonefile_instance()
1199 20
        return zoneinfo_file.get(name)
1200

1201 20
    def testZoneInfoFileStart1(self):
1202 20
        tz = self.gettz("EST5EDT")
1203 20
        self.assertEqual(datetime(2003, 4, 6, 1, 59, tzinfo=tz).tzname(), "EST",
1204
                         MISSING_TARBALL)
1205 20
        self.assertEqual(datetime(2003, 4, 6, 2, 00, tzinfo=tz).tzname(), "EDT")
1206

1207 20
    def testZoneInfoFileEnd1(self):
1208 20
        tzc = self.gettz("EST5EDT")
1209 20
        self.assertEqual(datetime(2003, 10, 26, 0, 59, tzinfo=tzc).tzname(),
1210
                         "EDT", MISSING_TARBALL)
1211

1212 20
        end_est = tz.enfold(datetime(2003, 10, 26, 1, 00, tzinfo=tzc), fold=1)
1213 20
        self.assertEqual(end_est.tzname(), "EST")
1214

1215 20
    def testZoneInfoOffsetSignal(self):
1216 20
        utc = self.gettz("UTC")
1217 20
        nyc = self.gettz("America/New_York")
1218 20
        self.assertNotEqual(utc, None, MISSING_TARBALL)
1219 20
        self.assertNotEqual(nyc, None)
1220 20
        t0 = datetime(2007, 11, 4, 0, 30, tzinfo=nyc)
1221 20
        t1 = t0.astimezone(utc)
1222 20
        t2 = t1.astimezone(nyc)
1223 20
        self.assertEqual(t0, t2)
1224 20
        self.assertEqual(nyc.dst(t0), timedelta(hours=1))
1225

1226 20
    def testZoneInfoCopy(self):
1227
        # copy.copy() called on a ZoneInfo file was returning the same instance
1228 20
        CHI = self.gettz('America/Chicago')
1229 20
        CHI_COPY = copy.copy(CHI)
1230

1231 20
        self.assertIsNot(CHI, CHI_COPY)
1232 20
        self.assertEqual(CHI, CHI_COPY)
1233

1234 20
    def testZoneInfoDeepCopy(self):
1235 20
        CHI = self.gettz('America/Chicago')
1236 20
        CHI_COPY = copy.deepcopy(CHI)
1237

1238 20
        self.assertIsNot(CHI, CHI_COPY)
1239 20
        self.assertEqual(CHI, CHI_COPY)
1240

1241 20
    def testZoneInfoInstanceCaching(self):
1242 20
        zif_0 = zoneinfo.get_zonefile_instance()
1243 20
        zif_1 = zoneinfo.get_zonefile_instance()
1244

1245 20
        self.assertIs(zif_0, zif_1)
1246

1247 20
    def testZoneInfoNewInstance(self):
1248 20
        zif_0 = zoneinfo.get_zonefile_instance()
1249 20
        zif_1 = zoneinfo.get_zonefile_instance(new_instance=True)
1250 20
        zif_2 = zoneinfo.get_zonefile_instance()
1251

1252 20
        self.assertIsNot(zif_0, zif_1)
1253 20
        self.assertIs(zif_1, zif_2)
1254

1255 20
    def testZoneInfoDeprecated(self):
1256 20
        with pytest.warns(DeprecationWarning):
1257 20
            zoneinfo.gettz('US/Eastern')
1258

1259 20
    def testZoneInfoMetadataDeprecated(self):
1260 20
        with pytest.warns(DeprecationWarning):
1261 20
            zoneinfo.gettz_db_metadata()
1262

1263

1264 20
class TZRangeTest(unittest.TestCase, TzFoldMixin):
1265 20
    TZ_EST = tz.tzrange('EST', timedelta(hours=-5),
1266
                        'EDT', timedelta(hours=-4),
1267
                        start=relativedelta(month=3, day=1, hour=2,
1268
                                            weekday=SU(+2)),
1269
                        end=relativedelta(month=11, day=1, hour=1,
1270
                                          weekday=SU(+1)))
1271

1272 20
    TZ_AEST = tz.tzrange('AEST', timedelta(hours=10),
1273
                         'AEDT', timedelta(hours=11),
1274
                         start=relativedelta(month=10, day=1, hour=2,
1275
                                             weekday=SU(+1)),
1276
                         end=relativedelta(month=4, day=1, hour=2,
1277
                                           weekday=SU(+1)))
1278

1279 20
    TZ_LON = tz.tzrange('GMT', timedelta(hours=0),
1280
                        'BST', timedelta(hours=1),
1281
                        start=relativedelta(month=3, day=31, weekday=SU(-1),
1282
                                            hours=2),
1283
                        end=relativedelta(month=10, day=31, weekday=SU(-1),
1284
                                          hours=1))
1285
    # POSIX string for UTC
1286 20
    UTC = 'UTC'
1287

1288 20
    def gettz(self, tzname):
1289 20
        tzname_map = {'Australia/Sydney': self.TZ_AEST,
1290
                      'America/Toronto': self.TZ_EST,
1291
                      'America/New_York': self.TZ_EST,
1292
                      'Europe/London': self.TZ_LON}
1293

1294 20
        return tzname_map[tzname]
1295

1296 20
    def testRangeCmp1(self):
1297 20
        self.assertEqual(tz.tzstr("EST5EDT"),
1298
                         tz.tzrange("EST", -18000, "EDT", -14400,
1299
                                 relativedelta(hours=+2,
1300
                                               month=4, day=1,
1301
                                               weekday=SU(+1)),
1302
                                 relativedelta(hours=+1,
1303
                                               month=10, day=31,
1304
                                               weekday=SU(-1))))
1305

1306 20
    def testRangeCmp2(self):
1307 20
        self.assertEqual(tz.tzstr("EST5EDT"),
1308
                         tz.tzrange("EST", -18000, "EDT"))
1309

1310 20
    def testRangeOffsets(self):
1311 20
        TZR = tz.tzrange('EST', -18000, 'EDT', -14400,
1312
                         start=relativedelta(hours=2, month=4, day=1,
1313
                                             weekday=SU(+2)),
1314
                         end=relativedelta(hours=1, month=10, day=31,
1315
                                           weekday=SU(-1)))
1316

1317 20
        dt_std = datetime(2014, 4, 11, 12, 0, tzinfo=TZR)  # STD
1318 20
        dt_dst = datetime(2016, 4, 11, 12, 0, tzinfo=TZR)  # DST
1319

1320 20
        dst_zero = timedelta(0)
1321 20
        dst_hour = timedelta(hours=1)
1322

1323 20
        std_offset = timedelta(hours=-5)
1324 20
        dst_offset = timedelta(hours=-4)
1325

1326
        # Check dst()
1327 20
        self.assertEqual(dt_std.dst(), dst_zero)
1328 20
        self.assertEqual(dt_dst.dst(), dst_hour)
1329

1330
        # Check utcoffset()
1331 20
        self.assertEqual(dt_std.utcoffset(), std_offset)
1332 20
        self.assertEqual(dt_dst.utcoffset(), dst_offset)
1333

1334
        # Check tzname
1335 20
        self.assertEqual(dt_std.tzname(), 'EST')
1336 20
        self.assertEqual(dt_dst.tzname(), 'EDT')
1337

1338 20
    def testTimeOnlyRangeFixed(self):
1339
        # This is a fixed-offset zone, so tzrange allows this
1340 20
        tz_range = tz.tzrange('dflt', stdoffset=timedelta(hours=-3))
1341 20
        self.assertEqual(dt_time(13, 20, tzinfo=tz_range).utcoffset(),
1342
                         timedelta(hours=-3))
1343

1344 20
    def testTimeOnlyRange(self):
1345
        # tzrange returns None because this zone has DST
1346 20
        tz_range = tz.tzrange('EST', timedelta(hours=-5),
1347
                              'EDT', timedelta(hours=-4))
1348 20
        self.assertIs(dt_time(13, 20, tzinfo=tz_range).utcoffset(), None)
1349

1350 20
    def testBrokenIsDstHandling(self):
1351
        # tzrange._isdst() was using a date() rather than a datetime().
1352
        # Issue reported by Lennart Regebro.
1353 20
        dt = datetime(2007, 8, 6, 4, 10, tzinfo=tz.UTC)
1354 20
        self.assertEqual(dt.astimezone(tz=tz.gettz("GMT+2")),
1355
                          datetime(2007, 8, 6, 6, 10, tzinfo=tz.tzstr("GMT+2")))
1356

1357 20
    def testRangeTimeDelta(self):
1358
        # Test that tzrange can be specified with a timedelta instead of an int.
1359 20
        EST5EDT_td = tz.tzrange('EST', timedelta(hours=-5),
1360
                                'EDT', timedelta(hours=-4))
1361

1362 20
        EST5EDT_sec = tz.tzrange('EST', -18000,
1363
                                 'EDT', -14400)
1364

1365 20
        self.assertEqual(EST5EDT_td, EST5EDT_sec)
1366

1367 20
    def testRangeEquality(self):
1368 20
        TZR1 = tz.tzrange('EST', -18000, 'EDT', -14400)
1369

1370
        # Standard abbreviation different
1371 20
        TZR2 = tz.tzrange('ET', -18000, 'EDT', -14400)
1372 20
        self.assertNotEqual(TZR1, TZR2)
1373

1374
        # DST abbreviation different
1375 20
        TZR3 = tz.tzrange('EST', -18000, 'EMT', -14400)
1376 20
        self.assertNotEqual(TZR1, TZR3)
1377

1378
        # STD offset different
1379 20
        TZR4 = tz.tzrange('EST', -14000, 'EDT', -14400)
1380 20
        self.assertNotEqual(TZR1, TZR4)
1381

1382
        # DST offset different
1383 20
        TZR5 = tz.tzrange('EST', -18000, 'EDT', -18000)
1384 20
        self.assertNotEqual(TZR1, TZR5)
1385

1386
        # Start delta different
1387 20
        TZR6 = tz.tzrange('EST', -18000, 'EDT', -14400,
1388
                          start=relativedelta(hours=+1, month=3,
1389
                                              day=1, weekday=SU(+2)))
1390 20
        self.assertNotEqual(TZR1, TZR6)
1391

1392
        # End delta different
1393 20
        TZR7 = tz.tzrange('EST', -18000, 'EDT', -14400,
1394
            end=relativedelta(hours=+1, month=11,
1395
                              day=1, weekday=SU(+2)))
1396 20
        self.assertNotEqual(TZR1, TZR7)
1397

1398 20
    def testRangeInequalityUnsupported(self):
1399 20
        TZR = tz.tzrange('EST', -18000, 'EDT', -14400)
1400

1401 20
        self.assertFalse(TZR == 4)
1402 20
        self.assertTrue(TZR == ComparesEqual)
1403 20
        self.assertFalse(TZR != ComparesEqual)
1404

1405

1406 20
@pytest.mark.tzstr
1407 20
class TZStrTest(unittest.TestCase, TzFoldMixin):
1408
    # POSIX string indicating change to summer time on the 2nd Sunday in March
1409
    # at 2AM, and ending the 1st Sunday in November at 2AM. (valid >= 2007)
1410 20
    TZ_EST = 'EST+5EDT,M3.2.0/2,M11.1.0/2'
1411

1412
    # POSIX string for AEST/AEDT (valid >= 2008)
1413 20
    TZ_AEST = 'AEST-10AEDT,M10.1.0/2,M4.1.0/3'
1414

1415
    # POSIX string for GMT/BST
1416 20
    TZ_LON = 'GMT0BST,M3.5.0,M10.5.0'
1417

1418 20
    def gettz(self, tzname):
1419
        # Actual time zone changes are handled by the _gettz_context function
1420 20
        tzname_map = {'Australia/Sydney': self.TZ_AEST,
1421
                      'America/Toronto': self.TZ_EST,
1422
                      'America/New_York': self.TZ_EST,
1423
                      'Europe/London': self.TZ_LON}
1424

1425 20
        return tz.tzstr(tzname_map[tzname])
1426

1427 20
    def testStrStr(self):
1428
        # Test that tz.tzstr() won't throw an error if given a str instead
1429
        # of a unicode literal.
1430 20
        self.assertEqual(datetime(2003, 4, 6, 1, 59,
1431
                                  tzinfo=tz.tzstr(str("EST5EDT"))).tzname(), "EST")
1432 20
        self.assertEqual(datetime(2003, 4, 6, 2, 00,
1433
                                  tzinfo=tz.tzstr(str("EST5EDT"))).tzname(), "EDT")
1434

1435 20
    def testStrInequality(self):
1436 20
        TZS1 = tz.tzstr('EST5EDT4')
1437

1438
        # Standard abbreviation different
1439 20
        TZS2 = tz.tzstr('ET5EDT4')
1440 20
        self.assertNotEqual(TZS1, TZS2)
1441

1442
        # DST abbreviation different
1443 20
        TZS3 = tz.tzstr('EST5EMT')
1444 20
        self.assertNotEqual(TZS1, TZS3)
1445

1446
        # STD offset different
1447 20
        TZS4 = tz.tzstr('EST4EDT4')
1448 20
        self.assertNotEqual(TZS1, TZS4)
1449

1450
        # DST offset different
1451 20
        TZS5 = tz.tzstr('EST5EDT3')
1452 20
        self.assertNotEqual(TZS1, TZS5)
1453

1454 20
    def testStrInequalityStartEnd(self):
1455 20
        TZS1 = tz.tzstr('EST5EDT4')
1456

1457
        # Start delta different
1458 20
        TZS2 = tz.tzstr('EST5EDT4,M4.2.0/02:00:00,M10-5-0/02:00')
1459 20
        self.assertNotEqual(TZS1, TZS2)
1460

1461
        # End delta different
1462 20
        TZS3 = tz.tzstr('EST5EDT4,M4.2.0/02:00:00,M11-5-0/02:00')
1463 20
        self.assertNotEqual(TZS1, TZS3)
1464

1465 20
    def testPosixOffset(self):
1466 20
        TZ1 = tz.tzstr('UTC-3')
1467 20
        self.assertEqual(datetime(2015, 1, 1, tzinfo=TZ1).utcoffset(),
1468
                         timedelta(hours=-3))
1469

1470 20
        TZ2 = tz.tzstr('UTC-3', posix_offset=True)
1471 20
        self.assertEqual(datetime(2015, 1, 1, tzinfo=TZ2).utcoffset(),
1472
                         timedelta(hours=+3))
1473

1474 20
    def testStrInequalityUnsupported(self):
1475 20
        TZS = tz.tzstr('EST5EDT')
1476

1477 20
        self.assertFalse(TZS == 4)
1478 20
        self.assertTrue(TZS == ComparesEqual)
1479 20
        self.assertFalse(TZS != ComparesEqual)
1480

1481 20
    def testTzStrRepr(self):
1482 20
        TZS1 = tz.tzstr('EST5EDT4')
1483 20
        TZS2 = tz.tzstr('EST')
1484

1485 20
        self.assertEqual(repr(TZS1), "tzstr(" + repr('EST5EDT4') + ")")
1486 20
        self.assertEqual(repr(TZS2), "tzstr(" + repr('EST') + ")")
1487

1488 20
    def testTzStrFailure(self):
1489 20
        with self.assertRaises(ValueError):
1490 20
            tz.tzstr('InvalidString;439999')
1491

1492 20
    def testTzStrSingleton(self):
1493 20
        tz1 = tz.tzstr('EST5EDT')
1494 20
        tz2 = tz.tzstr('CST4CST')
1495 20
        tz3 = tz.tzstr('EST5EDT')
1496

1497 20
        self.assertIsNot(tz1, tz2)
1498 20
        self.assertIs(tz1, tz3)
1499

1500 20
    def testTzStrSingletonPosix(self):
1501 20
        tz_t1 = tz.tzstr('GMT+3', posix_offset=True)
1502 20
        tz_f1 = tz.tzstr('GMT+3', posix_offset=False)
1503

1504 20
        tz_t2 = tz.tzstr('GMT+3', posix_offset=True)
1505 20
        tz_f2 = tz.tzstr('GMT+3', posix_offset=False)
1506

1507 20
        self.assertIs(tz_t1, tz_t2)
1508 20
        self.assertIsNot(tz_t1, tz_f1)
1509

1510 20
        self.assertIs(tz_f1, tz_f2)
1511

1512 20
    def testTzStrInstance(self):
1513 20
        tz1 = tz.tzstr('EST5EDT')
1514 20
        tz2 = tz.tzstr.instance('EST5EDT')
1515 20
        tz3 = tz.tzstr.instance('EST5EDT')
1516

1517 20
        assert tz1 is not tz2
1518 20
        assert tz2 is not tz3
1519

1520
        # Ensure that these still are all the same zone
1521 20
        assert tz1 == tz2 == tz3
1522

1523

1524 20
@pytest.mark.smoke
1525 20
@pytest.mark.tzstr
1526 2
def test_tzstr_weakref():
1527 20
    tz_t1 = tz.tzstr('EST5EDT')
1528 20
    tz_t2_ref = weakref.ref(tz.tzstr('EST5EDT'))
1529 20
    assert tz_t1 is tz_t2_ref()
1530

1531 20
    del tz_t1
1532 20
    gc.collect()
1533

1534 20
    assert tz_t2_ref() is not None
1535 20
    assert tz.tzstr('EST5EDT') is tz_t2_ref()
1536

1537 20
    for offset in range(5,15):
1538 20
        tz.tzstr('GMT+{}'.format(offset))
1539 20
    gc.collect()
1540

1541 20
    assert tz_t2_ref() is None
1542 20
    assert tz.tzstr('EST5EDT') is not tz_t2_ref()
1543

1544

1545 20
@pytest.mark.tzstr
1546 20
@pytest.mark.parametrize('tz_str,expected', [
1547
    # From https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html
1548
    ('', tz.tzrange(None)),     # TODO: Should change this so tz.tzrange('') works
1549
    ('EST+5EDT,M3.2.0/2,M11.1.0/12',
1550
     tz.tzrange('EST', -18000, 'EDT', -14400,
1551
        start=relativedelta(month=3, day=1, weekday=SU(2), hours=2),
1552
        end=relativedelta(month=11, day=1, weekday=SU(1), hours=11))),
1553
    ('WART4WARST,J1/0,J365/25',  # This is DST all year, Western Argentina Summer Time
1554
     tz.tzrange('WART', timedelta(hours=-4), 'WARST',
1555
        start=relativedelta(month=1, day=1, hours=0),
1556
        end=relativedelta(month=12, day=31, days=1))),
1557
    ('IST-2IDT,M3.4.4/26,M10.5.0',      # Israel Standard / Daylight Time
1558
     tz.tzrange('IST', timedelta(hours=2), 'IDT',
1559
        start=relativedelta(month=3, day=1, weekday=TH(4), days=1, hours=2),
1560
        end=relativedelta(month=10, day=31, weekday=SU(-1), hours=1))),
1561
    ('WGT3WGST,M3.5.0/2,M10.5.0/1',
1562
     tz.tzrange('WGT', timedelta(hours=-3), 'WGST',
1563
        start=relativedelta(month=3, day=31, weekday=SU(-1), hours=2),
1564
        end=relativedelta(month=10, day=31, weekday=SU(-1), hours=0))),
1565

1566
    # Different offset specifications
1567
    ('WGT0300WGST',
1568
     tz.tzrange('WGT', timedelta(hours=-3), 'WGST')),
1569
    ('WGT03:00WGST',
1570
     tz.tzrange('WGT', timedelta(hours=-3), 'WGST')),
1571
    ('AEST-1100AEDT',
1572
     tz.tzrange('AEST', timedelta(hours=11), 'AEDT')),
1573
    ('AEST-11:00AEDT',
1574
     tz.tzrange('AEST', timedelta(hours=11), 'AEDT')),
1575

1576
    # Different time formats
1577
    ('EST5EDT,M3.2.0/4:00,M11.1.0/3:00',
1578
     tz.tzrange('EST', timedelta(hours=-5), 'EDT',
1579
        start=relativedelta(month=3, day=1, weekday=SU(2), hours=4),
1580
        end=relativedelta(month=11, day=1, weekday=SU(1), hours=2))),
1581
    ('EST5EDT,M3.2.0/04:00,M11.1.0/03:00',
1582
     tz.tzrange('EST', timedelta(hours=-5), 'EDT',
1583
        start=relativedelta(month=3, day=1, weekday=SU(2), hours=4),
1584
        end=relativedelta(month=11, day=1, weekday=SU(1), hours=2))),
1585
    ('EST5EDT,M3.2.0/0400,M11.1.0/0300',
1586
     tz.tzrange('EST', timedelta(hours=-5), 'EDT',
1587
        start=relativedelta(month=3, day=1, weekday=SU(2), hours=4),
1588
        end=relativedelta(month=11, day=1, weekday=SU(1), hours=2))),
1589
])
1590 2
def test_valid_GNU_tzstr(tz_str, expected):
1591 20
    tzi = tz.tzstr(tz_str)
1592

1593 20
    assert tzi == expected
1594

1595

1596 20
@pytest.mark.tzstr
1597 20
@pytest.mark.parametrize('tz_str, expected', [
1598
    ('EST5EDT,5,4,0,7200,11,3,0,7200',
1599
     tz.tzrange('EST', timedelta(hours=-5), 'EDT',
1600
        start=relativedelta(month=5, day=1, weekday=SU(+4), hours=+2),
1601
        end=relativedelta(month=11, day=1, weekday=SU(+3), hours=+1))),
1602
    ('EST5EDT,5,-4,0,7200,11,3,0,7200',
1603
     tz.tzrange('EST', timedelta(hours=-5), 'EDT',
1604
        start=relativedelta(hours=+2, month=5, day=31, weekday=SU(-4)),
1605
        end=relativedelta(hours=+1, month=11, day=1, weekday=SU(+3)))),
1606
    ('EST5EDT,5,4,0,7200,11,-3,0,7200',
1607
     tz.tzrange('EST', timedelta(hours=-5), 'EDT',
1608
        start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)),
1609
        end=relativedelta(hours=+1, month=11, day=31, weekday=SU(-3)))),
1610
    ('EST5EDT,5,4,0,7200,11,-3,0,7200,3600',
1611
     tz.tzrange('EST', timedelta(hours=-5), 'EDT',
1612
        start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)),
1613
        end=relativedelta(hours=+1, month=11, day=31, weekday=SU(-3)))),
1614
    ('EST5EDT,5,4,0,7200,11,-3,0,7200,3600',
1615
     tz.tzrange('EST', timedelta(hours=-5), 'EDT',
1616
        start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)),
1617
        end=relativedelta(hours=+1, month=11, day=31, weekday=SU(-3)))),
1618
    ('EST5EDT,5,4,0,7200,11,-3,0,7200,-3600',
1619
     tz.tzrange('EST', timedelta(hours=-5), 'EDT', timedelta(hours=-6),
1620
        start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)),
1621
        end=relativedelta(hours=+3, month=11, day=31, weekday=SU(-3)))),
1622
    ('EST5EDT,5,4,0,7200,11,-3,0,7200,+7200',
1623
     tz.tzrange('EST', timedelta(hours=-5), 'EDT', timedelta(hours=-3),
1624
        start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)),
1625
        end=relativedelta(hours=0, month=11, day=31, weekday=SU(-3)))),
1626
    ('EST5EDT,5,4,0,7200,11,-3,0,7200,+3600',
1627
     tz.tzrange('EST', timedelta(hours=-5), 'EDT',
1628
        start=relativedelta(hours=+2, month=5, day=1, weekday=SU(+4)),
1629
        end=relativedelta(hours=+1, month=11, day=31, weekday=SU(-3)))),
1630
])
1631 2
def test_valid_dateutil_format(tz_str, expected):
1632
    # This tests the dateutil-specific format that is used widely in the tests
1633
    # and examples. It is unclear where this format originated from.
1634 20
    with pytest.warns(tz.DeprecatedTzFormatWarning):
1635 20
        tzi = tz.tzstr.instance(tz_str)
1636

1637 20
    assert tzi == expected
1638

1639

1640 20
@pytest.mark.tzstr
1641 20
@pytest.mark.parametrize('tz_str', [
1642
    'hdfiughdfuig,dfughdfuigpu87ñ::',
1643
    ',dfughdfuigpu87ñ::',
1644
    '-1:WART4WARST,J1,J365/25',
1645
    'WART4WARST,J1,J365/-25',
1646
    'IST-2IDT,M3.4.-1/26,M10.5.0',
1647
    'IST-2IDT,M3,2000,1/26,M10,5,0'
1648
])
1649 2
def test_invalid_GNU_tzstr(tz_str):
1650 20
    with pytest.raises(ValueError):
1651 20
        tz.tzstr(tz_str)
1652

1653

1654
# Different representations of the same default rule set
1655 20
DEFAULT_TZSTR_RULES_EQUIV_2003 = [
1656
    'EST5EDT',
1657
    'EST5EDT4,M4.1.0/02:00:00,M10-5-0/02:00',
1658
    'EST5EDT4,95/02:00:00,298/02:00',
1659
    'EST5EDT4,J96/02:00:00,J299/02:00',
1660
    'EST5EDT4,J96/02:00:00,J299/02'
1661
]
1662

1663

1664 20
@pytest.mark.tzstr
1665 20
@pytest.mark.parametrize('tz_str', DEFAULT_TZSTR_RULES_EQUIV_2003)
1666 2
def test_tzstr_default_start(tz_str):
1667 20
    tzi = tz.tzstr(tz_str)
1668 20
    dt_std = datetime(2003, 4, 6, 1, 59, tzinfo=tzi)
1669 20
    dt_dst = datetime(2003, 4, 6, 2, 00, tzinfo=tzi)
1670

1671 20
    assert get_timezone_tuple(dt_std) == EST_TUPLE
1672 20
    assert get_timezone_tuple(dt_dst) == EDT_TUPLE
1673

1674

1675 20
@pytest.mark.tzstr
1676 20
@pytest.mark.parametrize('tz_str', DEFAULT_TZSTR_RULES_EQUIV_2003)
1677 2
def test_tzstr_default_end(tz_str):
1678 20
    tzi = tz.tzstr(tz_str)
1679 20
    dt_dst = datetime(2003, 10, 26, 0, 59, tzinfo=tzi)
1680 20
    dt_dst_ambig = datetime(2003, 10, 26, 1, 00, tzinfo=tzi)
1681 20
    dt_std_ambig = tz.enfold(dt_dst_ambig, fold=1)
1682 20
    dt_std = datetime(2003, 10, 26, 2, 00, tzinfo=tzi)
1683

1684 20
    assert get_timezone_tuple(dt_dst) == EDT_TUPLE
1685 20
    assert get_timezone_tuple(dt_dst_ambig) == EDT_TUPLE
1686 20
    assert get_timezone_tuple(dt_std_ambig) == EST_TUPLE
1687 20
    assert get_timezone_tuple(dt_std) == EST_TUPLE
1688

1689

1690 20
@pytest.mark.tzstr
1691 20
@pytest.mark.parametrize('tzstr_1', ['EST5EDT',
1692
                                     'EST5EDT4,M4.1.0/02:00:00,M10-5-0/02:00'])
1693 20
@pytest.mark.parametrize('tzstr_2', ['EST5EDT',
1694
                                     'EST5EDT4,M4.1.0/02:00:00,M10-5-0/02:00'])
1695 2
def test_tzstr_default_cmp(tzstr_1, tzstr_2):
1696 20
    tz1 = tz.tzstr(tzstr_1)
1697 20
    tz2 = tz.tzstr(tzstr_2)
1698

1699 20
    assert tz1 == tz2
1700

1701 20
class TZICalTest(unittest.TestCase, TzFoldMixin):
1702 20
    def _gettz_str_tuple(self, tzname):
1703 20
        TZ_EST = (
1704
            'BEGIN:VTIMEZONE',
1705
            'TZID:US-Eastern',
1706
            'BEGIN:STANDARD',
1707
            'DTSTART:19971029T020000',
1708
            'RRULE:FREQ=YEARLY;BYDAY=+1SU;BYMONTH=11',
1709
            'TZOFFSETFROM:-0400',
1710
            'TZOFFSETTO:-0500',
1711
            'TZNAME:EST',
1712
            'END:STANDARD',
1713
            'BEGIN:DAYLIGHT',
1714
            'DTSTART:19980301T020000',
1715
            'RRULE:FREQ=YEARLY;BYDAY=+2SU;BYMONTH=03',
1716
            'TZOFFSETFROM:-0500',
1717
            'TZOFFSETTO:-0400',
1718
            'TZNAME:EDT',
1719
            'END:DAYLIGHT',
1720
            'END:VTIMEZONE'
1721
        )
1722

1723 20
        TZ_PST = (
1724
            'BEGIN:VTIMEZONE',
1725
            'TZID:US-Pacific',
1726
            'BEGIN:STANDARD',
1727
            'DTSTART:19971029T020000',
1728
            'RRULE:FREQ=YEARLY;BYDAY=+1SU;BYMONTH=11',
1729
            'TZOFFSETFROM:-0700',
1730
            'TZOFFSETTO:-0800',
1731
            'TZNAME:PST',
1732
            'END:STANDARD',
1733
            'BEGIN:DAYLIGHT',
1734
            'DTSTART:19980301T020000',
1735
            'RRULE:FREQ=YEARLY;BYDAY=+2SU;BYMONTH=03',
1736
            'TZOFFSETFROM:-0800',
1737
            'TZOFFSETTO:-0700',
1738
            'TZNAME:PDT',
1739
            'END:DAYLIGHT',
1740
            'END:VTIMEZONE'
1741
        )
1742

1743 20
        TZ_AEST = (
1744
            'BEGIN:VTIMEZONE',
1745
            'TZID:Australia-Sydney',
1746
            'BEGIN:STANDARD',
1747
            'DTSTART:19980301T030000',
1748
            'RRULE:FREQ=YEARLY;BYDAY=+1SU;BYMONTH=04',
1749
            'TZOFFSETFROM:+1100',
1750
            'TZOFFSETTO:+1000',
1751
            'TZNAME:AEST',
1752
            'END:STANDARD',
1753
            'BEGIN:DAYLIGHT',
1754
            'DTSTART:19971029T020000',
1755
            'RRULE:FREQ=YEARLY;BYDAY=+1SU;BYMONTH=10',
1756
            'TZOFFSETFROM:+1000',
1757
            'TZOFFSETTO:+1100',
1758
            'TZNAME:AEDT',
1759
            'END:DAYLIGHT',
1760
            'END:VTIMEZONE'
1761
        )
1762

1763 20
        TZ_LON = (
1764
            'BEGIN:VTIMEZONE',
1765
            'TZID:Europe-London',
1766
            'BEGIN:STANDARD',
1767
            'DTSTART:19810301T030000',
1768
            'RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10;BYHOUR=02',
1769
            'TZOFFSETFROM:+0100',
1770
            'TZOFFSETTO:+0000',
1771
            'TZNAME:GMT',
1772
            'END:STANDARD',
1773
            'BEGIN:DAYLIGHT',
1774
            'DTSTART:19961001T030000',
1775
            'RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=03;BYHOUR=01',
1776
            'TZOFFSETFROM:+0000',
1777
            'TZOFFSETTO:+0100',
1778
            'TZNAME:BST',
1779
            'END:DAYLIGHT',
1780
            'END:VTIMEZONE'
1781
        )
1782

1783 20
        tzname_map = {'Australia/Sydney': TZ_AEST,
1784
                      'America/Toronto': TZ_EST,
1785
                      'America/New_York': TZ_EST,
1786
                      'America/Los_Angeles': TZ_PST,
1787
                      'Europe/London': TZ_LON}
1788

1789 20
        return tzname_map[tzname]
1790

1791 20
    def _gettz_str(self, tzname):
1792 20
        return '\n'.join(self._gettz_str_tuple(tzname))
1793

1794 20
    def _tzstr_dtstart_with_params(self, tzname, param_str):
1795
        # Adds parameters to the DTSTART values of a given tzstr
1796 20
        tz_str_tuple = self._gettz_str_tuple(tzname)
1797

1798 20
        out_tz = []
1799 20
        for line in tz_str_tuple:
1800 20
            if line.startswith('DTSTART'):
1801 20
                name, value = line.split(':', 1)
1802 20
                line = name + ';' + param_str + ':' + value
1803

1804 20
            out_tz.append(line)
1805

1806 20
        return '\n'.join(out_tz)
1807

1808 20
    def gettz(self, tzname):
1809 20
        tz_str = self._gettz_str(tzname)
1810

1811 20
        tzc = tz.tzical(StringIO(tz_str)).get()
1812

1813 20
        return tzc
1814

1815 20
    def testRepr(self):
1816 20
        instr = StringIO(TZICAL_PST8PDT)
1817 20
        instr.name = 'StringIO(PST8PDT)'
1818 20
        tzc = tz.tzical(instr)
1819

1820 20
        self.assertEqual(repr(tzc), "tzical(" + repr(instr.name) + ")")
1821

1822
    # Test performance
1823 20
    def _test_us_zone(self, tzc, func, values, start):
1824 20
        if start:
1825 20
            dt1 = datetime(2003, 3, 9, 1, 59)
1826 20
            dt2 = datetime(2003, 3, 9, 2, 00)
1827 20
            fold = [0, 0]
1828
        else:
1829 20
            dt1 = datetime(2003, 11, 2, 0, 59)
1830 20
            dt2 = datetime(2003, 11, 2, 1, 00)
1831 20
            fold = [0, 1]
1832

1833 20
        dts = (tz.enfold(dt.replace(tzinfo=tzc), fold=f)
1834
               for dt, f in zip((dt1, dt2), fold))
1835

1836 20
        for value, dt in zip(values, dts):
1837 20
            self.assertEqual(func(dt), value)
1838

1839 20
    def _test_multi_zones(self, tzstrs, tzids, func, values, start):
1840 20
        tzic = tz.tzical(StringIO('\n'.join(tzstrs)))
1841 20
        for tzid, vals in zip(tzids, values):
1842 20
            tzc = tzic.get(tzid)
1843

1844 20
            self._test_us_zone(tzc, func, vals, start)
1845

1846 20
    def _prepare_EST(self):
1847 20
        tz_str = self._gettz_str('America/New_York')
1848 20
        return tz.tzical(StringIO(tz_str)).get()
1849

1850 20
    def _testEST(self, start, test_type, tzc=None):
1851 20
        if tzc is None:
1852 20
            tzc = self._prepare_EST()
1853

1854 20
        argdict = {
1855
            'name':   (datetime.tzname, ('EST', 'EDT')),
1856
            'offset': (datetime.utcoffset, (timedelta(hours=-5),
1857
                                            timedelta(hours=-4))),
1858
            'dst':    (datetime.dst, (timedelta(hours=0),
1859