• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1from six import PY2
2
3from functools import wraps
4
5from datetime import datetime, timedelta, tzinfo
6
7
8ZERO = timedelta(0)
9
10__all__ = ['tzname_in_python2', 'enfold']
11
12
13def tzname_in_python2(namefunc):
14    """Change unicode output into bytestrings in Python 2
15
16    tzname() API changed in Python 3. It used to return bytes, but was changed
17    to unicode strings
18    """
19    if PY2:
20        @wraps(namefunc)
21        def adjust_encoding(*args, **kwargs):
22            name = namefunc(*args, **kwargs)
23            if name is not None:
24                name = name.encode()
25
26            return name
27
28        return adjust_encoding
29    else:
30        return namefunc
31
32
33# The following is adapted from Alexander Belopolsky's tz library
34# https://github.com/abalkin/tz
35if hasattr(datetime, 'fold'):
36    # This is the pre-python 3.6 fold situation
37    def enfold(dt, fold=1):
38        """
39        Provides a unified interface for assigning the ``fold`` attribute to
40        datetimes both before and after the implementation of PEP-495.
41
42        :param fold:
43            The value for the ``fold`` attribute in the returned datetime. This
44            should be either 0 or 1.
45
46        :return:
47            Returns an object for which ``getattr(dt, 'fold', 0)`` returns
48            ``fold`` for all versions of Python. In versions prior to
49            Python 3.6, this is a ``_DatetimeWithFold`` object, which is a
50            subclass of :py:class:`datetime.datetime` with the ``fold``
51            attribute added, if ``fold`` is 1.
52
53        .. versionadded:: 2.6.0
54        """
55        return dt.replace(fold=fold)
56
57else:
58    class _DatetimeWithFold(datetime):
59        """
60        This is a class designed to provide a PEP 495-compliant interface for
61        Python versions before 3.6. It is used only for dates in a fold, so
62        the ``fold`` attribute is fixed at ``1``.
63
64        .. versionadded:: 2.6.0
65        """
66        __slots__ = ()
67
68        def replace(self, *args, **kwargs):
69            """
70            Return a datetime with the same attributes, except for those
71            attributes given new values by whichever keyword arguments are
72            specified. Note that tzinfo=None can be specified to create a naive
73            datetime from an aware datetime with no conversion of date and time
74            data.
75
76            This is reimplemented in ``_DatetimeWithFold`` because pypy3 will
77            return a ``datetime.datetime`` even if ``fold`` is unchanged.
78            """
79            argnames = (
80                'year', 'month', 'day', 'hour', 'minute', 'second',
81                'microsecond', 'tzinfo'
82            )
83
84            for arg, argname in zip(args, argnames):
85                if argname in kwargs:
86                    raise TypeError('Duplicate argument: {}'.format(argname))
87
88                kwargs[argname] = arg
89
90            for argname in argnames:
91                if argname not in kwargs:
92                    kwargs[argname] = getattr(self, argname)
93
94            dt_class = self.__class__ if kwargs.get('fold', 1) else datetime
95
96            return dt_class(**kwargs)
97
98        @property
99        def fold(self):
100            return 1
101
102    def enfold(dt, fold=1):
103        """
104        Provides a unified interface for assigning the ``fold`` attribute to
105        datetimes both before and after the implementation of PEP-495.
106
107        :param fold:
108            The value for the ``fold`` attribute in the returned datetime. This
109            should be either 0 or 1.
110
111        :return:
112            Returns an object for which ``getattr(dt, 'fold', 0)`` returns
113            ``fold`` for all versions of Python. In versions prior to
114            Python 3.6, this is a ``_DatetimeWithFold`` object, which is a
115            subclass of :py:class:`datetime.datetime` with the ``fold``
116            attribute added, if ``fold`` is 1.
117
118        .. versionadded:: 2.6.0
119        """
120        if getattr(dt, 'fold', 0) == fold:
121            return dt
122
123        args = dt.timetuple()[:6]
124        args += (dt.microsecond, dt.tzinfo)
125
126        if fold:
127            return _DatetimeWithFold(*args)
128        else:
129            return datetime(*args)
130
131
132def _validate_fromutc_inputs(f):
133    """
134    The CPython version of ``fromutc`` checks that the input is a ``datetime``
135    object and that ``self`` is attached as its ``tzinfo``.
136    """
137    @wraps(f)
138    def fromutc(self, dt):
139        if not isinstance(dt, datetime):
140            raise TypeError("fromutc() requires a datetime argument")
141        if dt.tzinfo is not self:
142            raise ValueError("dt.tzinfo is not self")
143
144        return f(self, dt)
145
146    return fromutc
147
148
149class _tzinfo(tzinfo):
150    """
151    Base class for all ``dateutil`` ``tzinfo`` objects.
152    """
153
154    def is_ambiguous(self, dt):
155        """
156        Whether or not the "wall time" of a given datetime is ambiguous in this
157        zone.
158
159        :param dt:
160            A :py:class:`datetime.datetime`, naive or time zone aware.
161
162
163        :return:
164            Returns ``True`` if ambiguous, ``False`` otherwise.
165
166        .. versionadded:: 2.6.0
167        """
168
169        dt = dt.replace(tzinfo=self)
170
171        wall_0 = enfold(dt, fold=0)
172        wall_1 = enfold(dt, fold=1)
173
174        same_offset = wall_0.utcoffset() == wall_1.utcoffset()
175        same_dt = wall_0.replace(tzinfo=None) == wall_1.replace(tzinfo=None)
176
177        return same_dt and not same_offset
178
179    def _fold_status(self, dt_utc, dt_wall):
180        """
181        Determine the fold status of a "wall" datetime, given a representation
182        of the same datetime as a (naive) UTC datetime. This is calculated based
183        on the assumption that ``dt.utcoffset() - dt.dst()`` is constant for all
184        datetimes, and that this offset is the actual number of hours separating
185        ``dt_utc`` and ``dt_wall``.
186
187        :param dt_utc:
188            Representation of the datetime as UTC
189
190        :param dt_wall:
191            Representation of the datetime as "wall time". This parameter must
192            either have a `fold` attribute or have a fold-naive
193            :class:`datetime.tzinfo` attached, otherwise the calculation may
194            fail.
195        """
196        if self.is_ambiguous(dt_wall):
197            delta_wall = dt_wall - dt_utc
198            _fold = int(delta_wall == (dt_utc.utcoffset() - dt_utc.dst()))
199        else:
200            _fold = 0
201
202        return _fold
203
204    def _fold(self, dt):
205        return getattr(dt, 'fold', 0)
206
207    def _fromutc(self, dt):
208        """
209        Given a timezone-aware datetime in a given timezone, calculates a
210        timezone-aware datetime in a new timezone.
211
212        Since this is the one time that we *know* we have an unambiguous
213        datetime object, we take this opportunity to determine whether the
214        datetime is ambiguous and in a "fold" state (e.g. if it's the first
215        occurence, chronologically, of the ambiguous datetime).
216
217        :param dt:
218            A timezone-aware :class:`datetime.datetime` object.
219        """
220
221        # Re-implement the algorithm from Python's datetime.py
222        dtoff = dt.utcoffset()
223        if dtoff is None:
224            raise ValueError("fromutc() requires a non-None utcoffset() "
225                             "result")
226
227        # The original datetime.py code assumes that `dst()` defaults to
228        # zero during ambiguous times. PEP 495 inverts this presumption, so
229        # for pre-PEP 495 versions of python, we need to tweak the algorithm.
230        dtdst = dt.dst()
231        if dtdst is None:
232            raise ValueError("fromutc() requires a non-None dst() result")
233        delta = dtoff - dtdst
234
235        dt += delta
236        # Set fold=1 so we can default to being in the fold for
237        # ambiguous dates.
238        dtdst = enfold(dt, fold=1).dst()
239        if dtdst is None:
240            raise ValueError("fromutc(): dt.dst gave inconsistent "
241                             "results; cannot convert")
242        return dt + dtdst
243
244    @_validate_fromutc_inputs
245    def fromutc(self, dt):
246        """
247        Given a timezone-aware datetime in a given timezone, calculates a
248        timezone-aware datetime in a new timezone.
249
250        Since this is the one time that we *know* we have an unambiguous
251        datetime object, we take this opportunity to determine whether the
252        datetime is ambiguous and in a "fold" state (e.g. if it's the first
253        occurance, chronologically, of the ambiguous datetime).
254
255        :param dt:
256            A timezone-aware :class:`datetime.datetime` object.
257        """
258        dt_wall = self._fromutc(dt)
259
260        # Calculate the fold status given the two datetimes.
261        _fold = self._fold_status(dt, dt_wall)
262
263        # Set the default fold value for ambiguous dates
264        return enfold(dt_wall, fold=_fold)
265
266
267class tzrangebase(_tzinfo):
268    """
269    This is an abstract base class for time zones represented by an annual
270    transition into and out of DST. Child classes should implement the following
271    methods:
272
273        * ``__init__(self, *args, **kwargs)``
274        * ``transitions(self, year)`` - this is expected to return a tuple of
275          datetimes representing the DST on and off transitions in standard
276          time.
277
278    A fully initialized ``tzrangebase`` subclass should also provide the
279    following attributes:
280        * ``hasdst``: Boolean whether or not the zone uses DST.
281        * ``_dst_offset`` / ``_std_offset``: :class:`datetime.timedelta` objects
282          representing the respective UTC offsets.
283        * ``_dst_abbr`` / ``_std_abbr``: Strings representing the timezone short
284          abbreviations in DST and STD, respectively.
285        * ``_hasdst``: Whether or not the zone has DST.
286
287    .. versionadded:: 2.6.0
288    """
289    def __init__(self):
290        raise NotImplementedError('tzrangebase is an abstract base class')
291
292    def utcoffset(self, dt):
293        isdst = self._isdst(dt)
294
295        if isdst is None:
296            return None
297        elif isdst:
298            return self._dst_offset
299        else:
300            return self._std_offset
301
302    def dst(self, dt):
303        isdst = self._isdst(dt)
304
305        if isdst is None:
306            return None
307        elif isdst:
308            return self._dst_base_offset
309        else:
310            return ZERO
311
312    @tzname_in_python2
313    def tzname(self, dt):
314        if self._isdst(dt):
315            return self._dst_abbr
316        else:
317            return self._std_abbr
318
319    def fromutc(self, dt):
320        """ Given a datetime in UTC, return local time """
321        if not isinstance(dt, datetime):
322            raise TypeError("fromutc() requires a datetime argument")
323
324        if dt.tzinfo is not self:
325            raise ValueError("dt.tzinfo is not self")
326
327        # Get transitions - if there are none, fixed offset
328        transitions = self.transitions(dt.year)
329        if transitions is None:
330            return dt + self.utcoffset(dt)
331
332        # Get the transition times in UTC
333        dston, dstoff = transitions
334
335        dston -= self._std_offset
336        dstoff -= self._std_offset
337
338        utc_transitions = (dston, dstoff)
339        dt_utc = dt.replace(tzinfo=None)
340
341        isdst = self._naive_isdst(dt_utc, utc_transitions)
342
343        if isdst:
344            dt_wall = dt + self._dst_offset
345        else:
346            dt_wall = dt + self._std_offset
347
348        _fold = int(not isdst and self.is_ambiguous(dt_wall))
349
350        return enfold(dt_wall, fold=_fold)
351
352    def is_ambiguous(self, dt):
353        """
354        Whether or not the "wall time" of a given datetime is ambiguous in this
355        zone.
356
357        :param dt:
358            A :py:class:`datetime.datetime`, naive or time zone aware.
359
360
361        :return:
362            Returns ``True`` if ambiguous, ``False`` otherwise.
363
364        .. versionadded:: 2.6.0
365        """
366        if not self.hasdst:
367            return False
368
369        start, end = self.transitions(dt.year)
370
371        dt = dt.replace(tzinfo=None)
372        return (end <= dt < end + self._dst_base_offset)
373
374    def _isdst(self, dt):
375        if not self.hasdst:
376            return False
377        elif dt is None:
378            return None
379
380        transitions = self.transitions(dt.year)
381
382        if transitions is None:
383            return False
384
385        dt = dt.replace(tzinfo=None)
386
387        isdst = self._naive_isdst(dt, transitions)
388
389        # Handle ambiguous dates
390        if not isdst and self.is_ambiguous(dt):
391            return not self._fold(dt)
392        else:
393            return isdst
394
395    def _naive_isdst(self, dt, transitions):
396        dston, dstoff = transitions
397
398        dt = dt.replace(tzinfo=None)
399
400        if dston < dstoff:
401            isdst = dston <= dt < dstoff
402        else:
403            isdst = not dstoff <= dt < dston
404
405        return isdst
406
407    @property
408    def _dst_base_offset(self):
409        return self._dst_offset - self._std_offset
410
411    __hash__ = None
412
413    def __ne__(self, other):
414        return not (self == other)
415
416    def __repr__(self):
417        return "%s(...)" % self.__class__.__name__
418
419    __reduce__ = object.__reduce__
420