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