• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# -*- coding: utf-8 -*-
2"""
3This module offers timezone implementations subclassing the abstract
4:py:`datetime.tzinfo` type. There are classes to handle tzfile format files
5(usually are in :file:`/etc/localtime`, :file:`/usr/share/zoneinfo`, etc), TZ
6environment string (in all known formats), given ranges (with help from
7relative deltas), local machine timezone, fixed offset timezone, and UTC
8timezone.
9"""
10import datetime
11import struct
12import time
13import sys
14import os
15import bisect
16
17import six
18from six import string_types
19from six.moves import _thread
20from ._common import tzname_in_python2, _tzinfo
21from ._common import tzrangebase, enfold
22from ._common import _validate_fromutc_inputs
23
24from ._factories import _TzSingleton, _TzOffsetFactory
25from ._factories import _TzStrFactory
26try:
27    from .win import tzwin, tzwinlocal
28except ImportError:
29    tzwin = tzwinlocal = None
30
31ZERO = datetime.timedelta(0)
32EPOCH = datetime.datetime.utcfromtimestamp(0)
33EPOCHORDINAL = EPOCH.toordinal()
34
35
36@six.add_metaclass(_TzSingleton)
37class tzutc(datetime.tzinfo):
38    """
39    This is a tzinfo object that represents the UTC time zone.
40
41    **Examples:**
42
43    .. doctest::
44
45        >>> from datetime import *
46        >>> from dateutil.tz import *
47
48        >>> datetime.now()
49        datetime.datetime(2003, 9, 27, 9, 40, 1, 521290)
50
51        >>> datetime.now(tzutc())
52        datetime.datetime(2003, 9, 27, 12, 40, 12, 156379, tzinfo=tzutc())
53
54        >>> datetime.now(tzutc()).tzname()
55        'UTC'
56
57    .. versionchanged:: 2.7.0
58        ``tzutc()`` is now a singleton, so the result of ``tzutc()`` will
59        always return the same object.
60
61        .. doctest::
62
63            >>> from dateutil.tz import tzutc, UTC
64            >>> tzutc() is tzutc()
65            True
66            >>> tzutc() is UTC
67            True
68    """
69    def utcoffset(self, dt):
70        return ZERO
71
72    def dst(self, dt):
73        return ZERO
74
75    @tzname_in_python2
76    def tzname(self, dt):
77        return "UTC"
78
79    def is_ambiguous(self, dt):
80        """
81        Whether or not the "wall time" of a given datetime is ambiguous in this
82        zone.
83
84        :param dt:
85            A :py:class:`datetime.datetime`, naive or time zone aware.
86
87
88        :return:
89            Returns ``True`` if ambiguous, ``False`` otherwise.
90
91        .. versionadded:: 2.6.0
92        """
93        return False
94
95    @_validate_fromutc_inputs
96    def fromutc(self, dt):
97        """
98        Fast track version of fromutc() returns the original ``dt`` object for
99        any valid :py:class:`datetime.datetime` object.
100        """
101        return dt
102
103    def __eq__(self, other):
104        if not isinstance(other, (tzutc, tzoffset)):
105            return NotImplemented
106
107        return (isinstance(other, tzutc) or
108                (isinstance(other, tzoffset) and other._offset == ZERO))
109
110    __hash__ = None
111
112    def __ne__(self, other):
113        return not (self == other)
114
115    def __repr__(self):
116        return "%s()" % self.__class__.__name__
117
118    __reduce__ = object.__reduce__
119
120
121@six.add_metaclass(_TzOffsetFactory)
122class tzoffset(datetime.tzinfo):
123    """
124    A simple class for representing a fixed offset from UTC.
125
126    :param name:
127        The timezone name, to be returned when ``tzname()`` is called.
128    :param offset:
129        The time zone offset in seconds, or (since version 2.6.0, represented
130        as a :py:class:`datetime.timedelta` object).
131    """
132    def __init__(self, name, offset):
133        self._name = name
134
135        try:
136            # Allow a timedelta
137            offset = offset.total_seconds()
138        except (TypeError, AttributeError):
139            pass
140        self._offset = datetime.timedelta(seconds=offset)
141
142    def utcoffset(self, dt):
143        return self._offset
144
145    def dst(self, dt):
146        return ZERO
147
148    @tzname_in_python2
149    def tzname(self, dt):
150        return self._name
151
152    @_validate_fromutc_inputs
153    def fromutc(self, dt):
154        return dt + self._offset
155
156    def is_ambiguous(self, dt):
157        """
158        Whether or not the "wall time" of a given datetime is ambiguous in this
159        zone.
160
161        :param dt:
162            A :py:class:`datetime.datetime`, naive or time zone aware.
163        :return:
164            Returns ``True`` if ambiguous, ``False`` otherwise.
165
166        .. versionadded:: 2.6.0
167        """
168        return False
169
170    def __eq__(self, other):
171        if not isinstance(other, tzoffset):
172            return NotImplemented
173
174        return self._offset == other._offset
175
176    __hash__ = None
177
178    def __ne__(self, other):
179        return not (self == other)
180
181    def __repr__(self):
182        return "%s(%s, %s)" % (self.__class__.__name__,
183                               repr(self._name),
184                               int(self._offset.total_seconds()))
185
186    __reduce__ = object.__reduce__
187
188
189class tzlocal(_tzinfo):
190    """
191    A :class:`tzinfo` subclass built around the ``time`` timezone functions.
192    """
193    def __init__(self):
194        super(tzlocal, self).__init__()
195
196        self._std_offset = datetime.timedelta(seconds=-time.timezone)
197        if time.daylight:
198            self._dst_offset = datetime.timedelta(seconds=-time.altzone)
199        else:
200            self._dst_offset = self._std_offset
201
202        self._dst_saved = self._dst_offset - self._std_offset
203        self._hasdst = bool(self._dst_saved)
204        self._tznames = tuple(time.tzname)
205
206    def utcoffset(self, dt):
207        if dt is None and self._hasdst:
208            return None
209
210        if self._isdst(dt):
211            return self._dst_offset
212        else:
213            return self._std_offset
214
215    def dst(self, dt):
216        if dt is None and self._hasdst:
217            return None
218
219        if self._isdst(dt):
220            return self._dst_offset - self._std_offset
221        else:
222            return ZERO
223
224    @tzname_in_python2
225    def tzname(self, dt):
226        return self._tznames[self._isdst(dt)]
227
228    def is_ambiguous(self, dt):
229        """
230        Whether or not the "wall time" of a given datetime is ambiguous in this
231        zone.
232
233        :param dt:
234            A :py:class:`datetime.datetime`, naive or time zone aware.
235
236
237        :return:
238            Returns ``True`` if ambiguous, ``False`` otherwise.
239
240        .. versionadded:: 2.6.0
241        """
242        naive_dst = self._naive_is_dst(dt)
243        return (not naive_dst and
244                (naive_dst != self._naive_is_dst(dt - self._dst_saved)))
245
246    def _naive_is_dst(self, dt):
247        timestamp = _datetime_to_timestamp(dt)
248        return time.localtime(timestamp + time.timezone).tm_isdst
249
250    def _isdst(self, dt, fold_naive=True):
251        # We can't use mktime here. It is unstable when deciding if
252        # the hour near to a change is DST or not.
253        #
254        # timestamp = time.mktime((dt.year, dt.month, dt.day, dt.hour,
255        #                         dt.minute, dt.second, dt.weekday(), 0, -1))
256        # return time.localtime(timestamp).tm_isdst
257        #
258        # The code above yields the following result:
259        #
260        # >>> import tz, datetime
261        # >>> t = tz.tzlocal()
262        # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
263        # 'BRDT'
264        # >>> datetime.datetime(2003,2,16,0,tzinfo=t).tzname()
265        # 'BRST'
266        # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
267        # 'BRST'
268        # >>> datetime.datetime(2003,2,15,22,tzinfo=t).tzname()
269        # 'BRDT'
270        # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
271        # 'BRDT'
272        #
273        # Here is a more stable implementation:
274        #
275        if not self._hasdst:
276            return False
277
278        # Check for ambiguous times:
279        dstval = self._naive_is_dst(dt)
280        fold = getattr(dt, 'fold', None)
281
282        if self.is_ambiguous(dt):
283            if fold is not None:
284                return not self._fold(dt)
285            else:
286                return True
287
288        return dstval
289
290    def __eq__(self, other):
291        if isinstance(other, tzlocal):
292            return (self._std_offset == other._std_offset and
293                    self._dst_offset == other._dst_offset)
294        elif isinstance(other, tzutc):
295            return (not self._hasdst and
296                    self._tznames[0] in {'UTC', 'GMT'} and
297                    self._std_offset == ZERO)
298        elif isinstance(other, tzoffset):
299            return (not self._hasdst and
300                    self._tznames[0] == other._name and
301                    self._std_offset == other._offset)
302        else:
303            return NotImplemented
304
305    __hash__ = None
306
307    def __ne__(self, other):
308        return not (self == other)
309
310    def __repr__(self):
311        return "%s()" % self.__class__.__name__
312
313    __reduce__ = object.__reduce__
314
315
316class _ttinfo(object):
317    __slots__ = ["offset", "delta", "isdst", "abbr",
318                 "isstd", "isgmt", "dstoffset"]
319
320    def __init__(self):
321        for attr in self.__slots__:
322            setattr(self, attr, None)
323
324    def __repr__(self):
325        l = []
326        for attr in self.__slots__:
327            value = getattr(self, attr)
328            if value is not None:
329                l.append("%s=%s" % (attr, repr(value)))
330        return "%s(%s)" % (self.__class__.__name__, ", ".join(l))
331
332    def __eq__(self, other):
333        if not isinstance(other, _ttinfo):
334            return NotImplemented
335
336        return (self.offset == other.offset and
337                self.delta == other.delta and
338                self.isdst == other.isdst and
339                self.abbr == other.abbr and
340                self.isstd == other.isstd and
341                self.isgmt == other.isgmt and
342                self.dstoffset == other.dstoffset)
343
344    __hash__ = None
345
346    def __ne__(self, other):
347        return not (self == other)
348
349    def __getstate__(self):
350        state = {}
351        for name in self.__slots__:
352            state[name] = getattr(self, name, None)
353        return state
354
355    def __setstate__(self, state):
356        for name in self.__slots__:
357            if name in state:
358                setattr(self, name, state[name])
359
360
361class _tzfile(object):
362    """
363    Lightweight class for holding the relevant transition and time zone
364    information read from binary tzfiles.
365    """
366    attrs = ['trans_list', 'trans_list_utc', 'trans_idx', 'ttinfo_list',
367             'ttinfo_std', 'ttinfo_dst', 'ttinfo_before', 'ttinfo_first']
368
369    def __init__(self, **kwargs):
370        for attr in self.attrs:
371            setattr(self, attr, kwargs.get(attr, None))
372
373
374class tzfile(_tzinfo):
375    """
376    This is a ``tzinfo`` subclass thant allows one to use the ``tzfile(5)``
377    format timezone files to extract current and historical zone information.
378
379    :param fileobj:
380        This can be an opened file stream or a file name that the time zone
381        information can be read from.
382
383    :param filename:
384        This is an optional parameter specifying the source of the time zone
385        information in the event that ``fileobj`` is a file object. If omitted
386        and ``fileobj`` is a file stream, this parameter will be set either to
387        ``fileobj``'s ``name`` attribute or to ``repr(fileobj)``.
388
389    See `Sources for Time Zone and Daylight Saving Time Data
390    <https://data.iana.org/time-zones/tz-link.html>`_ for more information. Time
391    zone files can be compiled from the `IANA Time Zone database files
392    <https://www.iana.org/time-zones>`_ with the `zic time zone compiler
393    <https://www.freebsd.org/cgi/man.cgi?query=zic&sektion=8>`_
394    """
395
396    def __init__(self, fileobj, filename=None):
397        super(tzfile, self).__init__()
398
399        file_opened_here = False
400        if isinstance(fileobj, string_types):
401            self._filename = fileobj
402            fileobj = open(fileobj, 'rb')
403            file_opened_here = True
404        elif filename is not None:
405            self._filename = filename
406        elif hasattr(fileobj, "name"):
407            self._filename = fileobj.name
408        else:
409            self._filename = repr(fileobj)
410
411        if fileobj is not None:
412            if not file_opened_here:
413                fileobj = _ContextWrapper(fileobj)
414
415            with fileobj as file_stream:
416                tzobj = self._read_tzfile(file_stream)
417
418            self._set_tzdata(tzobj)
419
420    def _set_tzdata(self, tzobj):
421        """ Set the time zone data of this object from a _tzfile object """
422        # Copy the relevant attributes over as private attributes
423        for attr in _tzfile.attrs:
424            setattr(self, '_' + attr, getattr(tzobj, attr))
425
426    def _read_tzfile(self, fileobj):
427        out = _tzfile()
428
429        # From tzfile(5):
430        #
431        # The time zone information files used by tzset(3)
432        # begin with the magic characters "TZif" to identify
433        # them as time zone information files, followed by
434        # sixteen bytes reserved for future use, followed by
435        # six four-byte values of type long, written in a
436        # ``standard'' byte order (the high-order  byte
437        # of the value is written first).
438        if fileobj.read(4).decode() != "TZif":
439            raise ValueError("magic not found")
440
441        fileobj.read(16)
442
443        (
444            # The number of UTC/local indicators stored in the file.
445            ttisgmtcnt,
446
447            # The number of standard/wall indicators stored in the file.
448            ttisstdcnt,
449
450            # The number of leap seconds for which data is
451            # stored in the file.
452            leapcnt,
453
454            # The number of "transition times" for which data
455            # is stored in the file.
456            timecnt,
457
458            # The number of "local time types" for which data
459            # is stored in the file (must not be zero).
460            typecnt,
461
462            # The  number  of  characters  of "time zone
463            # abbreviation strings" stored in the file.
464            charcnt,
465
466        ) = struct.unpack(">6l", fileobj.read(24))
467
468        # The above header is followed by tzh_timecnt four-byte
469        # values  of  type long,  sorted  in ascending order.
470        # These values are written in ``standard'' byte order.
471        # Each is used as a transition time (as  returned  by
472        # time(2)) at which the rules for computing local time
473        # change.
474
475        if timecnt:
476            out.trans_list_utc = list(struct.unpack(">%dl" % timecnt,
477                                                    fileobj.read(timecnt*4)))
478        else:
479            out.trans_list_utc = []
480
481        # Next come tzh_timecnt one-byte values of type unsigned
482        # char; each one tells which of the different types of
483        # ``local time'' types described in the file is associated
484        # with the same-indexed transition time. These values
485        # serve as indices into an array of ttinfo structures that
486        # appears next in the file.
487
488        if timecnt:
489            out.trans_idx = struct.unpack(">%dB" % timecnt,
490                                            fileobj.read(timecnt))
491        else:
492            out.trans_idx = []
493
494        # Each ttinfo structure is written as a four-byte value
495        # for tt_gmtoff  of  type long,  in  a  standard  byte
496        # order, followed  by a one-byte value for tt_isdst
497        # and a one-byte  value  for  tt_abbrind.   In  each
498        # structure, tt_gmtoff  gives  the  number  of
499        # seconds to be added to UTC, tt_isdst tells whether
500        # tm_isdst should be set by  localtime(3),  and
501        # tt_abbrind serves  as an index into the array of
502        # time zone abbreviation characters that follow the
503        # ttinfo structure(s) in the file.
504
505        ttinfo = []
506
507        for i in range(typecnt):
508            ttinfo.append(struct.unpack(">lbb", fileobj.read(6)))
509
510        abbr = fileobj.read(charcnt).decode()
511
512        # Then there are tzh_leapcnt pairs of four-byte
513        # values, written in  standard byte  order;  the
514        # first  value  of  each pair gives the time (as
515        # returned by time(2)) at which a leap second
516        # occurs;  the  second  gives the  total  number of
517        # leap seconds to be applied after the given time.
518        # The pairs of values are sorted in ascending order
519        # by time.
520
521        # Not used, for now (but seek for correct file position)
522        if leapcnt:
523            fileobj.seek(leapcnt * 8, os.SEEK_CUR)
524
525        # Then there are tzh_ttisstdcnt standard/wall
526        # indicators, each stored as a one-byte value;
527        # they tell whether the transition times associated
528        # with local time types were specified as standard
529        # time or wall clock time, and are used when
530        # a time zone file is used in handling POSIX-style
531        # time zone environment variables.
532
533        if ttisstdcnt:
534            isstd = struct.unpack(">%db" % ttisstdcnt,
535                                  fileobj.read(ttisstdcnt))
536
537        # Finally, there are tzh_ttisgmtcnt UTC/local
538        # indicators, each stored as a one-byte value;
539        # they tell whether the transition times associated
540        # with local time types were specified as UTC or
541        # local time, and are used when a time zone file
542        # is used in handling POSIX-style time zone envi-
543        # ronment variables.
544
545        if ttisgmtcnt:
546            isgmt = struct.unpack(">%db" % ttisgmtcnt,
547                                  fileobj.read(ttisgmtcnt))
548
549        # Build ttinfo list
550        out.ttinfo_list = []
551        for i in range(typecnt):
552            gmtoff, isdst, abbrind = ttinfo[i]
553            # Round to full-minutes if that's not the case. Python's
554            # datetime doesn't accept sub-minute timezones. Check
555            # http://python.org/sf/1447945 for some information.
556            gmtoff = 60 * ((gmtoff + 30) // 60)
557            tti = _ttinfo()
558            tti.offset = gmtoff
559            tti.dstoffset = datetime.timedelta(0)
560            tti.delta = datetime.timedelta(seconds=gmtoff)
561            tti.isdst = isdst
562            tti.abbr = abbr[abbrind:abbr.find('\x00', abbrind)]
563            tti.isstd = (ttisstdcnt > i and isstd[i] != 0)
564            tti.isgmt = (ttisgmtcnt > i and isgmt[i] != 0)
565            out.ttinfo_list.append(tti)
566
567        # Replace ttinfo indexes for ttinfo objects.
568        out.trans_idx = [out.ttinfo_list[idx] for idx in out.trans_idx]
569
570        # Set standard, dst, and before ttinfos. before will be
571        # used when a given time is before any transitions,
572        # and will be set to the first non-dst ttinfo, or to
573        # the first dst, if all of them are dst.
574        out.ttinfo_std = None
575        out.ttinfo_dst = None
576        out.ttinfo_before = None
577        if out.ttinfo_list:
578            if not out.trans_list_utc:
579                out.ttinfo_std = out.ttinfo_first = out.ttinfo_list[0]
580            else:
581                for i in range(timecnt-1, -1, -1):
582                    tti = out.trans_idx[i]
583                    if not out.ttinfo_std and not tti.isdst:
584                        out.ttinfo_std = tti
585                    elif not out.ttinfo_dst and tti.isdst:
586                        out.ttinfo_dst = tti
587
588                    if out.ttinfo_std and out.ttinfo_dst:
589                        break
590                else:
591                    if out.ttinfo_dst and not out.ttinfo_std:
592                        out.ttinfo_std = out.ttinfo_dst
593
594                for tti in out.ttinfo_list:
595                    if not tti.isdst:
596                        out.ttinfo_before = tti
597                        break
598                else:
599                    out.ttinfo_before = out.ttinfo_list[0]
600
601        # Now fix transition times to become relative to wall time.
602        #
603        # I'm not sure about this. In my tests, the tz source file
604        # is setup to wall time, and in the binary file isstd and
605        # isgmt are off, so it should be in wall time. OTOH, it's
606        # always in gmt time. Let me know if you have comments
607        # about this.
608        laststdoffset = None
609        out.trans_list = []
610        for i, tti in enumerate(out.trans_idx):
611            if not tti.isdst:
612                offset = tti.offset
613                laststdoffset = offset
614            else:
615                if laststdoffset is not None:
616                    # Store the DST offset as well and update it in the list
617                    tti.dstoffset = tti.offset - laststdoffset
618                    out.trans_idx[i] = tti
619
620                offset = laststdoffset or 0
621
622            out.trans_list.append(out.trans_list_utc[i] + offset)
623
624        # In case we missed any DST offsets on the way in for some reason, make
625        # a second pass over the list, looking for the /next/ DST offset.
626        laststdoffset = None
627        for i in reversed(range(len(out.trans_idx))):
628            tti = out.trans_idx[i]
629            if tti.isdst:
630                if not (tti.dstoffset or laststdoffset is None):
631                    tti.dstoffset = tti.offset - laststdoffset
632            else:
633                laststdoffset = tti.offset
634
635            if not isinstance(tti.dstoffset, datetime.timedelta):
636                tti.dstoffset = datetime.timedelta(seconds=tti.dstoffset)
637
638            out.trans_idx[i] = tti
639
640        out.trans_idx = tuple(out.trans_idx)
641        out.trans_list = tuple(out.trans_list)
642        out.trans_list_utc = tuple(out.trans_list_utc)
643
644        return out
645
646    def _find_last_transition(self, dt, in_utc=False):
647        # If there's no list, there are no transitions to find
648        if not self._trans_list:
649            return None
650
651        timestamp = _datetime_to_timestamp(dt)
652
653        # Find where the timestamp fits in the transition list - if the
654        # timestamp is a transition time, it's part of the "after" period.
655        trans_list = self._trans_list_utc if in_utc else self._trans_list
656        idx = bisect.bisect_right(trans_list, timestamp)
657
658        # We want to know when the previous transition was, so subtract off 1
659        return idx - 1
660
661    def _get_ttinfo(self, idx):
662        # For no list or after the last transition, default to _ttinfo_std
663        if idx is None or (idx + 1) >= len(self._trans_list):
664            return self._ttinfo_std
665
666        # If there is a list and the time is before it, return _ttinfo_before
667        if idx < 0:
668            return self._ttinfo_before
669
670        return self._trans_idx[idx]
671
672    def _find_ttinfo(self, dt):
673        idx = self._resolve_ambiguous_time(dt)
674
675        return self._get_ttinfo(idx)
676
677    def fromutc(self, dt):
678        """
679        The ``tzfile`` implementation of :py:func:`datetime.tzinfo.fromutc`.
680
681        :param dt:
682            A :py:class:`datetime.datetime` object.
683
684        :raises TypeError:
685            Raised if ``dt`` is not a :py:class:`datetime.datetime` object.
686
687        :raises ValueError:
688            Raised if this is called with a ``dt`` which does not have this
689            ``tzinfo`` attached.
690
691        :return:
692            Returns a :py:class:`datetime.datetime` object representing the
693            wall time in ``self``'s time zone.
694        """
695        # These isinstance checks are in datetime.tzinfo, so we'll preserve
696        # them, even if we don't care about duck typing.
697        if not isinstance(dt, datetime.datetime):
698            raise TypeError("fromutc() requires a datetime argument")
699
700        if dt.tzinfo is not self:
701            raise ValueError("dt.tzinfo is not self")
702
703        # First treat UTC as wall time and get the transition we're in.
704        idx = self._find_last_transition(dt, in_utc=True)
705        tti = self._get_ttinfo(idx)
706
707        dt_out = dt + datetime.timedelta(seconds=tti.offset)
708
709        fold = self.is_ambiguous(dt_out, idx=idx)
710
711        return enfold(dt_out, fold=int(fold))
712
713    def is_ambiguous(self, dt, idx=None):
714        """
715        Whether or not the "wall time" of a given datetime is ambiguous in this
716        zone.
717
718        :param dt:
719            A :py:class:`datetime.datetime`, naive or time zone aware.
720
721
722        :return:
723            Returns ``True`` if ambiguous, ``False`` otherwise.
724
725        .. versionadded:: 2.6.0
726        """
727        if idx is None:
728            idx = self._find_last_transition(dt)
729
730        # Calculate the difference in offsets from current to previous
731        timestamp = _datetime_to_timestamp(dt)
732        tti = self._get_ttinfo(idx)
733
734        if idx is None or idx <= 0:
735            return False
736
737        od = self._get_ttinfo(idx - 1).offset - tti.offset
738        tt = self._trans_list[idx]          # Transition time
739
740        return timestamp < tt + od
741
742    def _resolve_ambiguous_time(self, dt):
743        idx = self._find_last_transition(dt)
744
745        # If we have no transitions, return the index
746        _fold = self._fold(dt)
747        if idx is None or idx == 0:
748            return idx
749
750        # If it's ambiguous and we're in a fold, shift to a different index.
751        idx_offset = int(not _fold and self.is_ambiguous(dt, idx))
752
753        return idx - idx_offset
754
755    def utcoffset(self, dt):
756        if dt is None:
757            return None
758
759        if not self._ttinfo_std:
760            return ZERO
761
762        return self._find_ttinfo(dt).delta
763
764    def dst(self, dt):
765        if dt is None:
766            return None
767
768        if not self._ttinfo_dst:
769            return ZERO
770
771        tti = self._find_ttinfo(dt)
772
773        if not tti.isdst:
774            return ZERO
775
776        # The documentation says that utcoffset()-dst() must
777        # be constant for every dt.
778        return tti.dstoffset
779
780    @tzname_in_python2
781    def tzname(self, dt):
782        if not self._ttinfo_std or dt is None:
783            return None
784        return self._find_ttinfo(dt).abbr
785
786    def __eq__(self, other):
787        if not isinstance(other, tzfile):
788            return NotImplemented
789        return (self._trans_list == other._trans_list and
790                self._trans_idx == other._trans_idx and
791                self._ttinfo_list == other._ttinfo_list)
792
793    __hash__ = None
794
795    def __ne__(self, other):
796        return not (self == other)
797
798    def __repr__(self):
799        return "%s(%s)" % (self.__class__.__name__, repr(self._filename))
800
801    def __reduce__(self):
802        return self.__reduce_ex__(None)
803
804    def __reduce_ex__(self, protocol):
805        return (self.__class__, (None, self._filename), self.__dict__)
806
807
808class tzrange(tzrangebase):
809    """
810    The ``tzrange`` object is a time zone specified by a set of offsets and
811    abbreviations, equivalent to the way the ``TZ`` variable can be specified
812    in POSIX-like systems, but using Python delta objects to specify DST
813    start, end and offsets.
814
815    :param stdabbr:
816        The abbreviation for standard time (e.g. ``'EST'``).
817
818    :param stdoffset:
819        An integer or :class:`datetime.timedelta` object or equivalent
820        specifying the base offset from UTC.
821
822        If unspecified, +00:00 is used.
823
824    :param dstabbr:
825        The abbreviation for DST / "Summer" time (e.g. ``'EDT'``).
826
827        If specified, with no other DST information, DST is assumed to occur
828        and the default behavior or ``dstoffset``, ``start`` and ``end`` is
829        used. If unspecified and no other DST information is specified, it
830        is assumed that this zone has no DST.
831
832        If this is unspecified and other DST information is *is* specified,
833        DST occurs in the zone but the time zone abbreviation is left
834        unchanged.
835
836    :param dstoffset:
837        A an integer or :class:`datetime.timedelta` object or equivalent
838        specifying the UTC offset during DST. If unspecified and any other DST
839        information is specified, it is assumed to be the STD offset +1 hour.
840
841    :param start:
842        A :class:`relativedelta.relativedelta` object or equivalent specifying
843        the time and time of year that daylight savings time starts. To specify,
844        for example, that DST starts at 2AM on the 2nd Sunday in March, pass:
845
846            ``relativedelta(hours=2, month=3, day=1, weekday=SU(+2))``
847
848        If unspecified and any other DST information is specified, the default
849        value is 2 AM on the first Sunday in April.
850
851    :param end:
852        A :class:`relativedelta.relativedelta` object or equivalent representing
853        the time and time of year that daylight savings time ends, with the
854        same specification method as in ``start``. One note is that this should
855        point to the first time in the *standard* zone, so if a transition
856        occurs at 2AM in the DST zone and the clocks are set back 1 hour to 1AM,
857        set the `hours` parameter to +1.
858
859
860    **Examples:**
861
862    .. testsetup:: tzrange
863
864        from dateutil.tz import tzrange, tzstr
865
866    .. doctest:: tzrange
867
868        >>> tzstr('EST5EDT') == tzrange("EST", -18000, "EDT")
869        True
870
871        >>> from dateutil.relativedelta import *
872        >>> range1 = tzrange("EST", -18000, "EDT")
873        >>> range2 = tzrange("EST", -18000, "EDT", -14400,
874        ...                  relativedelta(hours=+2, month=4, day=1,
875        ...                                weekday=SU(+1)),
876        ...                  relativedelta(hours=+1, month=10, day=31,
877        ...                                weekday=SU(-1)))
878        >>> tzstr('EST5EDT') == range1 == range2
879        True
880
881    """
882    def __init__(self, stdabbr, stdoffset=None,
883                 dstabbr=None, dstoffset=None,
884                 start=None, end=None):
885
886        global relativedelta
887        from dateutil import relativedelta
888
889        self._std_abbr = stdabbr
890        self._dst_abbr = dstabbr
891
892        try:
893            stdoffset = stdoffset.total_seconds()
894        except (TypeError, AttributeError):
895            pass
896
897        try:
898            dstoffset = dstoffset.total_seconds()
899        except (TypeError, AttributeError):
900            pass
901
902        if stdoffset is not None:
903            self._std_offset = datetime.timedelta(seconds=stdoffset)
904        else:
905            self._std_offset = ZERO
906
907        if dstoffset is not None:
908            self._dst_offset = datetime.timedelta(seconds=dstoffset)
909        elif dstabbr and stdoffset is not None:
910            self._dst_offset = self._std_offset + datetime.timedelta(hours=+1)
911        else:
912            self._dst_offset = ZERO
913
914        if dstabbr and start is None:
915            self._start_delta = relativedelta.relativedelta(
916                hours=+2, month=4, day=1, weekday=relativedelta.SU(+1))
917        else:
918            self._start_delta = start
919
920        if dstabbr and end is None:
921            self._end_delta = relativedelta.relativedelta(
922                hours=+1, month=10, day=31, weekday=relativedelta.SU(-1))
923        else:
924            self._end_delta = end
925
926        self._dst_base_offset_ = self._dst_offset - self._std_offset
927        self.hasdst = bool(self._start_delta)
928
929    def transitions(self, year):
930        """
931        For a given year, get the DST on and off transition times, expressed
932        always on the standard time side. For zones with no transitions, this
933        function returns ``None``.
934
935        :param year:
936            The year whose transitions you would like to query.
937
938        :return:
939            Returns a :class:`tuple` of :class:`datetime.datetime` objects,
940            ``(dston, dstoff)`` for zones with an annual DST transition, or
941            ``None`` for fixed offset zones.
942        """
943        if not self.hasdst:
944            return None
945
946        base_year = datetime.datetime(year, 1, 1)
947
948        start = base_year + self._start_delta
949        end = base_year + self._end_delta
950
951        return (start, end)
952
953    def __eq__(self, other):
954        if not isinstance(other, tzrange):
955            return NotImplemented
956
957        return (self._std_abbr == other._std_abbr and
958                self._dst_abbr == other._dst_abbr and
959                self._std_offset == other._std_offset and
960                self._dst_offset == other._dst_offset and
961                self._start_delta == other._start_delta and
962                self._end_delta == other._end_delta)
963
964    @property
965    def _dst_base_offset(self):
966        return self._dst_base_offset_
967
968
969@six.add_metaclass(_TzStrFactory)
970class tzstr(tzrange):
971    """
972    ``tzstr`` objects are time zone objects specified by a time-zone string as
973    it would be passed to a ``TZ`` variable on POSIX-style systems (see
974    the `GNU C Library: TZ Variable`_ for more details).
975
976    There is one notable exception, which is that POSIX-style time zones use an
977    inverted offset format, so normally ``GMT+3`` would be parsed as an offset
978    3 hours *behind* GMT. The ``tzstr`` time zone object will parse this as an
979    offset 3 hours *ahead* of GMT. If you would like to maintain the POSIX
980    behavior, pass a ``True`` value to ``posix_offset``.
981
982    The :class:`tzrange` object provides the same functionality, but is
983    specified using :class:`relativedelta.relativedelta` objects. rather than
984    strings.
985
986    :param s:
987        A time zone string in ``TZ`` variable format. This can be a
988        :class:`bytes` (2.x: :class:`str`), :class:`str` (2.x: :class:`unicode`)
989        or a stream emitting unicode characters (e.g. :class:`StringIO`).
990
991    :param posix_offset:
992        Optional. If set to ``True``, interpret strings such as ``GMT+3`` or
993        ``UTC+3`` as being 3 hours *behind* UTC rather than ahead, per the
994        POSIX standard.
995
996    .. caution::
997
998        Prior to version 2.7.0, this function also supported time zones
999        in the format:
1000
1001            * ``EST5EDT,4,0,6,7200,10,0,26,7200,3600``
1002            * ``EST5EDT,4,1,0,7200,10,-1,0,7200,3600``
1003
1004        This format is non-standard and has been deprecated; this function
1005        will raise a :class:`DeprecatedTZFormatWarning` until
1006        support is removed in a future version.
1007
1008    .. _`GNU C Library: TZ Variable`:
1009        https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html
1010    """
1011    def __init__(self, s, posix_offset=False):
1012        global parser
1013        from dateutil.parser import _parser as parser
1014
1015        self._s = s
1016
1017        res = parser._parsetz(s)
1018        if res is None or res.any_unused_tokens:
1019            raise ValueError("unknown string format")
1020
1021        # Here we break the compatibility with the TZ variable handling.
1022        # GMT-3 actually *means* the timezone -3.
1023        if res.stdabbr in ("GMT", "UTC") and not posix_offset:
1024            res.stdoffset *= -1
1025
1026        # We must initialize it first, since _delta() needs
1027        # _std_offset and _dst_offset set. Use False in start/end
1028        # to avoid building it two times.
1029        tzrange.__init__(self, res.stdabbr, res.stdoffset,
1030                         res.dstabbr, res.dstoffset,
1031                         start=False, end=False)
1032
1033        if not res.dstabbr:
1034            self._start_delta = None
1035            self._end_delta = None
1036        else:
1037            self._start_delta = self._delta(res.start)
1038            if self._start_delta:
1039                self._end_delta = self._delta(res.end, isend=1)
1040
1041        self.hasdst = bool(self._start_delta)
1042
1043    def _delta(self, x, isend=0):
1044        from dateutil import relativedelta
1045        kwargs = {}
1046        if x.month is not None:
1047            kwargs["month"] = x.month
1048            if x.weekday is not None:
1049                kwargs["weekday"] = relativedelta.weekday(x.weekday, x.week)
1050                if x.week > 0:
1051                    kwargs["day"] = 1
1052                else:
1053                    kwargs["day"] = 31
1054            elif x.day:
1055                kwargs["day"] = x.day
1056        elif x.yday is not None:
1057            kwargs["yearday"] = x.yday
1058        elif x.jyday is not None:
1059            kwargs["nlyearday"] = x.jyday
1060        if not kwargs:
1061            # Default is to start on first sunday of april, and end
1062            # on last sunday of october.
1063            if not isend:
1064                kwargs["month"] = 4
1065                kwargs["day"] = 1
1066                kwargs["weekday"] = relativedelta.SU(+1)
1067            else:
1068                kwargs["month"] = 10
1069                kwargs["day"] = 31
1070                kwargs["weekday"] = relativedelta.SU(-1)
1071        if x.time is not None:
1072            kwargs["seconds"] = x.time
1073        else:
1074            # Default is 2AM.
1075            kwargs["seconds"] = 7200
1076        if isend:
1077            # Convert to standard time, to follow the documented way
1078            # of working with the extra hour. See the documentation
1079            # of the tzinfo class.
1080            delta = self._dst_offset - self._std_offset
1081            kwargs["seconds"] -= delta.seconds + delta.days * 86400
1082        return relativedelta.relativedelta(**kwargs)
1083
1084    def __repr__(self):
1085        return "%s(%s)" % (self.__class__.__name__, repr(self._s))
1086
1087
1088class _tzicalvtzcomp(object):
1089    def __init__(self, tzoffsetfrom, tzoffsetto, isdst,
1090                 tzname=None, rrule=None):
1091        self.tzoffsetfrom = datetime.timedelta(seconds=tzoffsetfrom)
1092        self.tzoffsetto = datetime.timedelta(seconds=tzoffsetto)
1093        self.tzoffsetdiff = self.tzoffsetto - self.tzoffsetfrom
1094        self.isdst = isdst
1095        self.tzname = tzname
1096        self.rrule = rrule
1097
1098
1099class _tzicalvtz(_tzinfo):
1100    def __init__(self, tzid, comps=[]):
1101        super(_tzicalvtz, self).__init__()
1102
1103        self._tzid = tzid
1104        self._comps = comps
1105        self._cachedate = []
1106        self._cachecomp = []
1107        self._cache_lock = _thread.allocate_lock()
1108
1109    def _find_comp(self, dt):
1110        if len(self._comps) == 1:
1111            return self._comps[0]
1112
1113        dt = dt.replace(tzinfo=None)
1114
1115        try:
1116            with self._cache_lock:
1117                return self._cachecomp[self._cachedate.index(
1118                    (dt, self._fold(dt)))]
1119        except ValueError:
1120            pass
1121
1122        lastcompdt = None
1123        lastcomp = None
1124
1125        for comp in self._comps:
1126            compdt = self._find_compdt(comp, dt)
1127
1128            if compdt and (not lastcompdt or lastcompdt < compdt):
1129                lastcompdt = compdt
1130                lastcomp = comp
1131
1132        if not lastcomp:
1133            # RFC says nothing about what to do when a given
1134            # time is before the first onset date. We'll look for the
1135            # first standard component, or the first component, if
1136            # none is found.
1137            for comp in self._comps:
1138                if not comp.isdst:
1139                    lastcomp = comp
1140                    break
1141            else:
1142                lastcomp = comp[0]
1143
1144        with self._cache_lock:
1145            self._cachedate.insert(0, (dt, self._fold(dt)))
1146            self._cachecomp.insert(0, lastcomp)
1147
1148            if len(self._cachedate) > 10:
1149                self._cachedate.pop()
1150                self._cachecomp.pop()
1151
1152        return lastcomp
1153
1154    def _find_compdt(self, comp, dt):
1155        if comp.tzoffsetdiff < ZERO and self._fold(dt):
1156            dt -= comp.tzoffsetdiff
1157
1158        compdt = comp.rrule.before(dt, inc=True)
1159
1160        return compdt
1161
1162    def utcoffset(self, dt):
1163        if dt is None:
1164            return None
1165
1166        return self._find_comp(dt).tzoffsetto
1167
1168    def dst(self, dt):
1169        comp = self._find_comp(dt)
1170        if comp.isdst:
1171            return comp.tzoffsetdiff
1172        else:
1173            return ZERO
1174
1175    @tzname_in_python2
1176    def tzname(self, dt):
1177        return self._find_comp(dt).tzname
1178
1179    def __repr__(self):
1180        return "<tzicalvtz %s>" % repr(self._tzid)
1181
1182    __reduce__ = object.__reduce__
1183
1184
1185class tzical(object):
1186    """
1187    This object is designed to parse an iCalendar-style ``VTIMEZONE`` structure
1188    as set out in `RFC 5545`_ Section 4.6.5 into one or more `tzinfo` objects.
1189
1190    :param `fileobj`:
1191        A file or stream in iCalendar format, which should be UTF-8 encoded
1192        with CRLF endings.
1193
1194    .. _`RFC 5545`: https://tools.ietf.org/html/rfc5545
1195    """
1196    def __init__(self, fileobj):
1197        global rrule
1198        from dateutil import rrule
1199
1200        if isinstance(fileobj, string_types):
1201            self._s = fileobj
1202            # ical should be encoded in UTF-8 with CRLF
1203            fileobj = open(fileobj, 'r')
1204        else:
1205            self._s = getattr(fileobj, 'name', repr(fileobj))
1206            fileobj = _ContextWrapper(fileobj)
1207
1208        self._vtz = {}
1209
1210        with fileobj as fobj:
1211            self._parse_rfc(fobj.read())
1212
1213    def keys(self):
1214        """
1215        Retrieves the available time zones as a list.
1216        """
1217        return list(self._vtz.keys())
1218
1219    def get(self, tzid=None):
1220        """
1221        Retrieve a :py:class:`datetime.tzinfo` object by its ``tzid``.
1222
1223        :param tzid:
1224            If there is exactly one time zone available, omitting ``tzid``
1225            or passing :py:const:`None` value returns it. Otherwise a valid
1226            key (which can be retrieved from :func:`keys`) is required.
1227
1228        :raises ValueError:
1229            Raised if ``tzid`` is not specified but there are either more
1230            or fewer than 1 zone defined.
1231
1232        :returns:
1233            Returns either a :py:class:`datetime.tzinfo` object representing
1234            the relevant time zone or :py:const:`None` if the ``tzid`` was
1235            not found.
1236        """
1237        if tzid is None:
1238            if len(self._vtz) == 0:
1239                raise ValueError("no timezones defined")
1240            elif len(self._vtz) > 1:
1241                raise ValueError("more than one timezone available")
1242            tzid = next(iter(self._vtz))
1243
1244        return self._vtz.get(tzid)
1245
1246    def _parse_offset(self, s):
1247        s = s.strip()
1248        if not s:
1249            raise ValueError("empty offset")
1250        if s[0] in ('+', '-'):
1251            signal = (-1, +1)[s[0] == '+']
1252            s = s[1:]
1253        else:
1254            signal = +1
1255        if len(s) == 4:
1256            return (int(s[:2]) * 3600 + int(s[2:]) * 60) * signal
1257        elif len(s) == 6:
1258            return (int(s[:2]) * 3600 + int(s[2:4]) * 60 + int(s[4:])) * signal
1259        else:
1260            raise ValueError("invalid offset: " + s)
1261
1262    def _parse_rfc(self, s):
1263        lines = s.splitlines()
1264        if not lines:
1265            raise ValueError("empty string")
1266
1267        # Unfold
1268        i = 0
1269        while i < len(lines):
1270            line = lines[i].rstrip()
1271            if not line:
1272                del lines[i]
1273            elif i > 0 and line[0] == " ":
1274                lines[i-1] += line[1:]
1275                del lines[i]
1276            else:
1277                i += 1
1278
1279        tzid = None
1280        comps = []
1281        invtz = False
1282        comptype = None
1283        for line in lines:
1284            if not line:
1285                continue
1286            name, value = line.split(':', 1)
1287            parms = name.split(';')
1288            if not parms:
1289                raise ValueError("empty property name")
1290            name = parms[0].upper()
1291            parms = parms[1:]
1292            if invtz:
1293                if name == "BEGIN":
1294                    if value in ("STANDARD", "DAYLIGHT"):
1295                        # Process component
1296                        pass
1297                    else:
1298                        raise ValueError("unknown component: "+value)
1299                    comptype = value
1300                    founddtstart = False
1301                    tzoffsetfrom = None
1302                    tzoffsetto = None
1303                    rrulelines = []
1304                    tzname = None
1305                elif name == "END":
1306                    if value == "VTIMEZONE":
1307                        if comptype:
1308                            raise ValueError("component not closed: "+comptype)
1309                        if not tzid:
1310                            raise ValueError("mandatory TZID not found")
1311                        if not comps:
1312                            raise ValueError(
1313                                "at least one component is needed")
1314                        # Process vtimezone
1315                        self._vtz[tzid] = _tzicalvtz(tzid, comps)
1316                        invtz = False
1317                    elif value == comptype:
1318                        if not founddtstart:
1319                            raise ValueError("mandatory DTSTART not found")
1320                        if tzoffsetfrom is None:
1321                            raise ValueError(
1322                                "mandatory TZOFFSETFROM not found")
1323                        if tzoffsetto is None:
1324                            raise ValueError(
1325                                "mandatory TZOFFSETFROM not found")
1326                        # Process component
1327                        rr = None
1328                        if rrulelines:
1329                            rr = rrule.rrulestr("\n".join(rrulelines),
1330                                                compatible=True,
1331                                                ignoretz=True,
1332                                                cache=True)
1333                        comp = _tzicalvtzcomp(tzoffsetfrom, tzoffsetto,
1334                                              (comptype == "DAYLIGHT"),
1335                                              tzname, rr)
1336                        comps.append(comp)
1337                        comptype = None
1338                    else:
1339                        raise ValueError("invalid component end: "+value)
1340                elif comptype:
1341                    if name == "DTSTART":
1342                        # DTSTART in VTIMEZONE takes a subset of valid RRULE
1343                        # values under RFC 5545.
1344                        for parm in parms:
1345                            if parm != 'VALUE=DATE-TIME':
1346                                msg = ('Unsupported DTSTART param in ' +
1347                                       'VTIMEZONE: ' + parm)
1348                                raise ValueError(msg)
1349                        rrulelines.append(line)
1350                        founddtstart = True
1351                    elif name in ("RRULE", "RDATE", "EXRULE", "EXDATE"):
1352                        rrulelines.append(line)
1353                    elif name == "TZOFFSETFROM":
1354                        if parms:
1355                            raise ValueError(
1356                                "unsupported %s parm: %s " % (name, parms[0]))
1357                        tzoffsetfrom = self._parse_offset(value)
1358                    elif name == "TZOFFSETTO":
1359                        if parms:
1360                            raise ValueError(
1361                                "unsupported TZOFFSETTO parm: "+parms[0])
1362                        tzoffsetto = self._parse_offset(value)
1363                    elif name == "TZNAME":
1364                        if parms:
1365                            raise ValueError(
1366                                "unsupported TZNAME parm: "+parms[0])
1367                        tzname = value
1368                    elif name == "COMMENT":
1369                        pass
1370                    else:
1371                        raise ValueError("unsupported property: "+name)
1372                else:
1373                    if name == "TZID":
1374                        if parms:
1375                            raise ValueError(
1376                                "unsupported TZID parm: "+parms[0])
1377                        tzid = value
1378                    elif name in ("TZURL", "LAST-MODIFIED", "COMMENT"):
1379                        pass
1380                    else:
1381                        raise ValueError("unsupported property: "+name)
1382            elif name == "BEGIN" and value == "VTIMEZONE":
1383                tzid = None
1384                comps = []
1385                invtz = True
1386
1387    def __repr__(self):
1388        return "%s(%s)" % (self.__class__.__name__, repr(self._s))
1389
1390
1391if sys.platform != "win32":
1392    TZFILES = ["/etc/localtime", "localtime"]
1393    TZPATHS = ["/usr/share/zoneinfo",
1394               "/usr/lib/zoneinfo",
1395               "/usr/share/lib/zoneinfo",
1396               "/etc/zoneinfo"]
1397else:
1398    TZFILES = []
1399    TZPATHS = []
1400
1401def __get_gettz():
1402    tzlocal_classes = (tzlocal,)
1403    if tzwinlocal is not None:
1404        tzlocal_classes += (tzwinlocal,)
1405
1406    class GettzFunc(object):
1407        def __init__(self):
1408
1409            self.__instances = {}
1410            self._cache_lock = _thread.allocate_lock()
1411
1412        def __call__(self, name=None):
1413            with self._cache_lock:
1414                rv = self.__instances.get(name, None)
1415
1416                if rv is None:
1417                    rv = self.nocache(name=name)
1418                    if not (name is None or isinstance(rv, tzlocal_classes)):
1419                        # tzlocal is slightly more complicated than the other
1420                        # time zone providers because it depends on environment
1421                        # at construction time, so don't cache that.
1422                        self.__instances[name] = rv
1423
1424            return rv
1425
1426        def cache_clear(self):
1427            with self._cache_lock:
1428                self.__instances = {}
1429
1430        @staticmethod
1431        def nocache(name=None):
1432            """A non-cached version of gettz"""
1433            tz = None
1434            if not name:
1435                try:
1436                    name = os.environ["TZ"]
1437                except KeyError:
1438                    pass
1439            if name is None or name == ":":
1440                for filepath in TZFILES:
1441                    if not os.path.isabs(filepath):
1442                        filename = filepath
1443                        for path in TZPATHS:
1444                            filepath = os.path.join(path, filename)
1445                            if os.path.isfile(filepath):
1446                                break
1447                        else:
1448                            continue
1449                    if os.path.isfile(filepath):
1450                        try:
1451                            tz = tzfile(filepath)
1452                            break
1453                        except (IOError, OSError, ValueError):
1454                            pass
1455                else:
1456                    tz = tzlocal()
1457            else:
1458                if name.startswith(":"):
1459                    name = name[1:]
1460                if os.path.isabs(name):
1461                    if os.path.isfile(name):
1462                        tz = tzfile(name)
1463                    else:
1464                        tz = None
1465                else:
1466                    for path in TZPATHS:
1467                        filepath = os.path.join(path, name)
1468                        if not os.path.isfile(filepath):
1469                            filepath = filepath.replace(' ', '_')
1470                            if not os.path.isfile(filepath):
1471                                continue
1472                        try:
1473                            tz = tzfile(filepath)
1474                            break
1475                        except (IOError, OSError, ValueError):
1476                            pass
1477                    else:
1478                        tz = None
1479                        if tzwin is not None:
1480                            try:
1481                                tz = tzwin(name)
1482                            except WindowsError:
1483                                tz = None
1484
1485                        if not tz:
1486                            from dateutil.zoneinfo import get_zonefile_instance
1487                            tz = get_zonefile_instance().get(name)
1488
1489                        if not tz:
1490                            for c in name:
1491                                # name must have at least one offset to be a tzstr
1492                                if c in "0123456789":
1493                                    try:
1494                                        tz = tzstr(name)
1495                                    except ValueError:
1496                                        pass
1497                                    break
1498                            else:
1499                                if name in ("GMT", "UTC"):
1500                                    tz = tzutc()
1501                                elif name in time.tzname:
1502                                    tz = tzlocal()
1503            return tz
1504
1505    return GettzFunc()
1506
1507gettz = __get_gettz()
1508del __get_gettz
1509
1510def datetime_exists(dt, tz=None):
1511    """
1512    Given a datetime and a time zone, determine whether or not a given datetime
1513    would fall in a gap.
1514
1515    :param dt:
1516        A :class:`datetime.datetime` (whose time zone will be ignored if ``tz``
1517        is provided.)
1518
1519    :param tz:
1520        A :class:`datetime.tzinfo` with support for the ``fold`` attribute. If
1521        ``None`` or not provided, the datetime's own time zone will be used.
1522
1523    :return:
1524        Returns a boolean value whether or not the "wall time" exists in ``tz``.
1525
1526    ..versionadded:: 2.7.0
1527    """
1528    if tz is None:
1529        if dt.tzinfo is None:
1530            raise ValueError('Datetime is naive and no time zone provided.')
1531        tz = dt.tzinfo
1532
1533    dt = dt.replace(tzinfo=None)
1534
1535    # This is essentially a test of whether or not the datetime can survive
1536    # a round trip to UTC.
1537    dt_rt = dt.replace(tzinfo=tz).astimezone(tzutc()).astimezone(tz)
1538    dt_rt = dt_rt.replace(tzinfo=None)
1539
1540    return dt == dt_rt
1541
1542
1543def datetime_ambiguous(dt, tz=None):
1544    """
1545    Given a datetime and a time zone, determine whether or not a given datetime
1546    is ambiguous (i.e if there are two times differentiated only by their DST
1547    status).
1548
1549    :param dt:
1550        A :class:`datetime.datetime` (whose time zone will be ignored if ``tz``
1551        is provided.)
1552
1553    :param tz:
1554        A :class:`datetime.tzinfo` with support for the ``fold`` attribute. If
1555        ``None`` or not provided, the datetime's own time zone will be used.
1556
1557    :return:
1558        Returns a boolean value whether or not the "wall time" is ambiguous in
1559        ``tz``.
1560
1561    .. versionadded:: 2.6.0
1562    """
1563    if tz is None:
1564        if dt.tzinfo is None:
1565            raise ValueError('Datetime is naive and no time zone provided.')
1566
1567        tz = dt.tzinfo
1568
1569    # If a time zone defines its own "is_ambiguous" function, we'll use that.
1570    is_ambiguous_fn = getattr(tz, 'is_ambiguous', None)
1571    if is_ambiguous_fn is not None:
1572        try:
1573            return tz.is_ambiguous(dt)
1574        except:
1575            pass
1576
1577    # If it doesn't come out and tell us it's ambiguous, we'll just check if
1578    # the fold attribute has any effect on this particular date and time.
1579    dt = dt.replace(tzinfo=tz)
1580    wall_0 = enfold(dt, fold=0)
1581    wall_1 = enfold(dt, fold=1)
1582
1583    same_offset = wall_0.utcoffset() == wall_1.utcoffset()
1584    same_dst = wall_0.dst() == wall_1.dst()
1585
1586    return not (same_offset and same_dst)
1587
1588
1589def resolve_imaginary(dt):
1590    """
1591    Given a datetime that may be imaginary, return an existing datetime.
1592
1593    This function assumes that an imaginary datetime represents what the
1594    wall time would be in a zone had the offset transition not occurred, so
1595    it will always fall forward by the transition's change in offset.
1596
1597    ..doctest::
1598        >>> from dateutil import tz
1599        >>> from datetime import datetime
1600        >>> NYC = tz.gettz('America/New_York')
1601        >>> print(tz.resolve_imaginary(datetime(2017, 3, 12, 2, 30, tzinfo=NYC)))
1602        2017-03-12 03:30:00-04:00
1603
1604        >>> KIR = tz.gettz('Pacific/Kiritimati')
1605        >>> print(tz.resolve_imaginary(datetime(1995, 1, 1, 12, 30, tzinfo=KIR)))
1606        1995-01-02 12:30:00+14:00
1607
1608    As a note, :func:`datetime.astimezone` is guaranteed to produce a valid,
1609    existing datetime, so a round-trip to and from UTC is sufficient to get
1610    an extant datetime, however, this generally "falls back" to an earlier time
1611    rather than falling forward to the STD side (though no guarantees are made
1612    about this behavior).
1613
1614    :param dt:
1615        A :class:`datetime.datetime` which may or may not exist.
1616
1617    :return:
1618        Returns an existing :class:`datetime.datetime`. If ``dt`` was not
1619        imaginary, the datetime returned is guaranteed to be the same object
1620        passed to the function.
1621
1622    ..versionadded:: 2.7.0
1623    """
1624    if dt.tzinfo is not None and not datetime_exists(dt):
1625
1626        curr_offset = (dt + datetime.timedelta(hours=24)).utcoffset()
1627        old_offset = (dt - datetime.timedelta(hours=24)).utcoffset()
1628
1629        dt += curr_offset - old_offset
1630
1631    return dt
1632
1633
1634def _datetime_to_timestamp(dt):
1635    """
1636    Convert a :class:`datetime.datetime` object to an epoch timestamp in seconds
1637    since January 1, 1970, ignoring the time zone.
1638    """
1639    return (dt.replace(tzinfo=None) - EPOCH).total_seconds()
1640
1641
1642class _ContextWrapper(object):
1643    """
1644    Class for wrapping contexts so that they are passed through in a
1645    with statement.
1646    """
1647    def __init__(self, context):
1648        self.context = context
1649
1650    def __enter__(self):
1651        return self.context
1652
1653    def __exit__(*args, **kwargs):
1654        pass
1655
1656# vim:ts=4:sw=4:et
1657