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 occurrence, 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 occurrence, 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__