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