codekingpro/portable-devtools
114k
1from six import PY22 3from functools import wraps4 5from datetime import datetime, timedelta, tzinfo6 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 215 16 tzname() API changed in Python 3. It used to return bytes, but was changed17 to unicode strings18 """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 name27 28 return adjust_encoding29 else:30 return namefunc31 32 33# The following is adapted from Alexander Belopolsky's tz library34# https://github.com/abalkin/tz35if hasattr(datetime, 'fold'):36 # This is the pre-python 3.6 fold situation37 def enfold(dt, fold=1):38 """39 Provides a unified interface for assigning the ``fold`` attribute to40 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. This44 should be either 0 or 1.45 46 :return:47 Returns an object for which ``getattr(dt, 'fold', 0)`` returns48 ``fold`` for all versions of Python. In versions prior to49 Python 3.6, this is a ``_DatetimeWithFold`` object, which is a50 subclass of :py:class:`datetime.datetime` with the ``fold``51 attribute added, if ``fold`` is 1.52 53 .. versionadded:: 2.6.054 """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 for61 Python versions before 3.6. It is used only for dates in a fold, so62 the ``fold`` attribute is fixed at ``1``.63 64 .. versionadded:: 2.6.065 """66 __slots__ = ()67 68 def replace(self, *args, **kwargs):69 """70 Return a datetime with the same attributes, except for those71 attributes given new values by whichever keyword arguments are72 specified. Note that tzinfo=None can be specified to create a naive73 datetime from an aware datetime with no conversion of date and time74 data.75 76 This is reimplemented in ``_DatetimeWithFold`` because pypy3 will77 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] = arg89 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 datetime95 96 return dt_class(**kwargs)97 98 @property99 def fold(self):100 return 1101 102 def enfold(dt, fold=1):103 """104 Provides a unified interface for assigning the ``fold`` attribute to105 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. This109 should be either 0 or 1.110 111 :return:112 Returns an object for which ``getattr(dt, 'fold', 0)`` returns113 ``fold`` for all versions of Python. In versions prior to114 Python 3.6, this is a ``_DatetimeWithFold`` object, which is a115 subclass of :py:class:`datetime.datetime` with the ``fold``116 attribute added, if ``fold`` is 1.117 118 .. versionadded:: 2.6.0119 """120 if getattr(dt, 'fold', 0) == fold:121 return dt122 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 fromutc147 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 this157 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.0167 """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_offset178 179 def _fold_status(self, dt_utc, dt_wall):180 """181 Determine the fold status of a "wall" datetime, given a representation182 of the same datetime as a (naive) UTC datetime. This is calculated based183 on the assumption that ``dt.utcoffset() - dt.dst()`` is constant for all184 datetimes, and that this offset is the actual number of hours separating185 ``dt_utc`` and ``dt_wall``.186 187 :param dt_utc:188 Representation of the datetime as UTC189 190 :param dt_wall:191 Representation of the datetime as "wall time". This parameter must192 either have a `fold` attribute or have a fold-naive193 :class:`datetime.tzinfo` attached, otherwise the calculation may194 fail.195 """196 if self.is_ambiguous(dt_wall):197 delta_wall = dt_wall - dt_utc198 _fold = int(delta_wall == (dt_utc.utcoffset() - dt_utc.dst()))199 else:200 _fold = 0201 202 return _fold203 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 a210 timezone-aware datetime in a new timezone.211 212 Since this is the one time that we *know* we have an unambiguous213 datetime object, we take this opportunity to determine whether the214 datetime is ambiguous and in a "fold" state (e.g. if it's the first215 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.py222 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 to228 # zero during ambiguous times. PEP 495 inverts this presumption, so229 # 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 - dtdst234 235 dt += delta236 # Set fold=1 so we can default to being in the fold for237 # 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 + dtdst243 244 @_validate_fromutc_inputs245 def fromutc(self, dt):246 """247 Given a timezone-aware datetime in a given timezone, calculates a248 timezone-aware datetime in a new timezone.249 250 Since this is the one time that we *know* we have an unambiguous251 datetime object, we take this opportunity to determine whether the252 datetime is ambiguous and in a "fold" state (e.g. if it's the first253 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 dates264 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 annual270 transition into and out of DST. Child classes should implement the following271 methods:272 273 * ``__init__(self, *args, **kwargs)``274 * ``transitions(self, year)`` - this is expected to return a tuple of275 datetimes representing the DST on and off transitions in standard276 time.277 278 A fully initialized ``tzrangebase`` subclass should also provide the279 following attributes:280 * ``hasdst``: Boolean whether or not the zone uses DST.281 * ``_dst_offset`` / ``_std_offset``: :class:`datetime.timedelta` objects282 representing the respective UTC offsets.283 * ``_dst_abbr`` / ``_std_abbr``: Strings representing the timezone short284 abbreviations in DST and STD, respectively.285 * ``_hasdst``: Whether or not the zone has DST.286 287 .. versionadded:: 2.6.0288 """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 None297 elif isdst:298 return self._dst_offset299 else:300 return self._std_offset301 302 def dst(self, dt):303 isdst = self._isdst(dt)304 305 if isdst is None:306 return None307 elif isdst:308 return self._dst_base_offset309 else:310 return ZERO311 312 @tzname_in_python2313 def tzname(self, dt):314 if self._isdst(dt):315 return self._dst_abbr316 else:317 return self._std_abbr318 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 offset328 transitions = self.transitions(dt.year)329 if transitions is None:330 return dt + self.utcoffset(dt)331 332 # Get the transition times in UTC333 dston, dstoff = transitions334 335 dston -= self._std_offset336 dstoff -= self._std_offset337 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_offset345 else:346 dt_wall = dt + self._std_offset347 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 this355 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.0365 """366 if not self.hasdst:367 return False368 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 False377 elif dt is None:378 return None379 380 transitions = self.transitions(dt.year)381 382 if transitions is None:383 return False384 385 dt = dt.replace(tzinfo=None)386 387 isdst = self._naive_isdst(dt, transitions)388 389 # Handle ambiguous dates390 if not isdst and self.is_ambiguous(dt):391 return not self._fold(dt)392 else:393 return isdst394 395 def _naive_isdst(self, dt, transitions):396 dston, dstoff = transitions397 398 dt = dt.replace(tzinfo=None)399 400 if dston < dstoff:401 isdst = dston <= dt < dstoff402 else:403 isdst = not dstoff <= dt < dston404 405 return isdst406 407 @property408 def _dst_base_offset(self):409 return self._dst_offset - self._std_offset410 411 __hash__ = None412 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 