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