_common.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. from six import PY3
  2. from six.moves import _thread
  3. from datetime import datetime, timedelta, tzinfo
  4. import copy
  5. ZERO = timedelta(0)
  6. __all__ = ['tzname_in_python2', 'enfold']
  7. def tzname_in_python2(namefunc):
  8. """Change unicode output into bytestrings in Python 2
  9. tzname() API changed in Python 3. It used to return bytes, but was changed
  10. to unicode strings
  11. """
  12. def adjust_encoding(*args, **kwargs):
  13. name = namefunc(*args, **kwargs)
  14. if name is not None and not PY3:
  15. name = name.encode()
  16. return name
  17. return adjust_encoding
  18. # The following is adapted from Alexander Belopolsky's tz library
  19. # https://github.com/abalkin/tz
  20. if hasattr(datetime, 'fold'):
  21. # This is the pre-python 3.6 fold situation
  22. def enfold(dt, fold=1):
  23. """
  24. Provides a unified interface for assigning the ``fold`` attribute to
  25. datetimes both before and after the implementation of PEP-495.
  26. :param fold:
  27. The value for the ``fold`` attribute in the returned datetime. This
  28. should be either 0 or 1.
  29. :return:
  30. Returns an object for which ``getattr(dt, 'fold', 0)`` returns
  31. ``fold`` for all versions of Python. In versions prior to
  32. Python 3.6, this is a ``_DatetimeWithFold`` object, which is a
  33. subclass of :py:class:`datetime.datetime` with the ``fold``
  34. attribute added, if ``fold`` is 1.
  35. ..versionadded:: 2.6.0
  36. """
  37. return dt.replace(fold=fold)
  38. else:
  39. class _DatetimeWithFold(datetime):
  40. """
  41. This is a class designed to provide a PEP 495-compliant interface for
  42. Python versions before 3.6. It is used only for dates in a fold, so
  43. the ``fold`` attribute is fixed at ``1``.
  44. ..versionadded:: 2.6.0
  45. """
  46. __slots__ = ()
  47. @property
  48. def fold(self):
  49. return 1
  50. def enfold(dt, fold=1):
  51. """
  52. Provides a unified interface for assigning the ``fold`` attribute to
  53. datetimes both before and after the implementation of PEP-495.
  54. :param fold:
  55. The value for the ``fold`` attribute in the returned datetime. This
  56. should be either 0 or 1.
  57. :return:
  58. Returns an object for which ``getattr(dt, 'fold', 0)`` returns
  59. ``fold`` for all versions of Python. In versions prior to
  60. Python 3.6, this is a ``_DatetimeWithFold`` object, which is a
  61. subclass of :py:class:`datetime.datetime` with the ``fold``
  62. attribute added, if ``fold`` is 1.
  63. ..versionadded:: 2.6.0
  64. """
  65. if getattr(dt, 'fold', 0) == fold:
  66. return dt
  67. args = dt.timetuple()[:6]
  68. args += (dt.microsecond, dt.tzinfo)
  69. if fold:
  70. return _DatetimeWithFold(*args)
  71. else:
  72. return datetime(*args)
  73. class _tzinfo(tzinfo):
  74. """
  75. Base class for all ``dateutil`` ``tzinfo`` objects.
  76. """
  77. def is_ambiguous(self, dt):
  78. """
  79. Whether or not the "wall time" of a given datetime is ambiguous in this
  80. zone.
  81. :param dt:
  82. A :py:class:`datetime.datetime`, naive or time zone aware.
  83. :return:
  84. Returns ``True`` if ambiguous, ``False`` otherwise.
  85. ..versionadded:: 2.6.0
  86. """
  87. dt = dt.replace(tzinfo=self)
  88. wall_0 = enfold(dt, fold=0)
  89. wall_1 = enfold(dt, fold=1)
  90. same_offset = wall_0.utcoffset() == wall_1.utcoffset()
  91. same_dt = wall_0.replace(tzinfo=None) == wall_1.replace(tzinfo=None)
  92. return same_dt and not same_offset
  93. def _fold_status(self, dt_utc, dt_wall):
  94. """
  95. Determine the fold status of a "wall" datetime, given a representation
  96. of the same datetime as a (naive) UTC datetime. This is calculated based
  97. on the assumption that ``dt.utcoffset() - dt.dst()`` is constant for all
  98. datetimes, and that this offset is the actual number of hours separating
  99. ``dt_utc`` and ``dt_wall``.
  100. :param dt_utc:
  101. Representation of the datetime as UTC
  102. :param dt_wall:
  103. Representation of the datetime as "wall time". This parameter must
  104. either have a `fold` attribute or have a fold-naive
  105. :class:`datetime.tzinfo` attached, otherwise the calculation may
  106. fail.
  107. """
  108. if self.is_ambiguous(dt_wall):
  109. delta_wall = dt_wall - dt_utc
  110. _fold = int(delta_wall == (dt_utc.utcoffset() - dt_utc.dst()))
  111. else:
  112. _fold = 0
  113. return _fold
  114. def _fold(self, dt):
  115. return getattr(dt, 'fold', 0)
  116. def _fromutc(self, dt):
  117. """
  118. Given a timezone-aware datetime in a given timezone, calculates a
  119. timezone-aware datetime in a new timezone.
  120. Since this is the one time that we *know* we have an unambiguous
  121. datetime object, we take this opportunity to determine whether the
  122. datetime is ambiguous and in a "fold" state (e.g. if it's the first
  123. occurence, chronologically, of the ambiguous datetime).
  124. :param dt:
  125. A timezone-aware :class:`datetime.dateime` object.
  126. """
  127. # Re-implement the algorithm from Python's datetime.py
  128. if not isinstance(dt, datetime):
  129. raise TypeError("fromutc() requires a datetime argument")
  130. if dt.tzinfo is not self:
  131. raise ValueError("dt.tzinfo is not self")
  132. dtoff = dt.utcoffset()
  133. if dtoff is None:
  134. raise ValueError("fromutc() requires a non-None utcoffset() "
  135. "result")
  136. # The original datetime.py code assumes that `dst()` defaults to
  137. # zero during ambiguous times. PEP 495 inverts this presumption, so
  138. # for pre-PEP 495 versions of python, we need to tweak the algorithm.
  139. dtdst = dt.dst()
  140. if dtdst is None:
  141. raise ValueError("fromutc() requires a non-None dst() result")
  142. delta = dtoff - dtdst
  143. if delta:
  144. dt += delta
  145. # Set fold=1 so we can default to being in the fold for
  146. # ambiguous dates.
  147. dtdst = enfold(dt, fold=1).dst()
  148. if dtdst is None:
  149. raise ValueError("fromutc(): dt.dst gave inconsistent "
  150. "results; cannot convert")
  151. return dt + dtdst
  152. def fromutc(self, dt):
  153. """
  154. Given a timezone-aware datetime in a given timezone, calculates a
  155. timezone-aware datetime in a new timezone.
  156. Since this is the one time that we *know* we have an unambiguous
  157. datetime object, we take this opportunity to determine whether the
  158. datetime is ambiguous and in a "fold" state (e.g. if it's the first
  159. occurance, chronologically, of the ambiguous datetime).
  160. :param dt:
  161. A timezone-aware :class:`datetime.dateime` object.
  162. """
  163. dt_wall = self._fromutc(dt)
  164. # Calculate the fold status given the two datetimes.
  165. _fold = self._fold_status(dt, dt_wall)
  166. # Set the default fold value for ambiguous dates
  167. return enfold(dt_wall, fold=_fold)
  168. class tzrangebase(_tzinfo):
  169. """
  170. This is an abstract base class for time zones represented by an annual
  171. transition into and out of DST. Child classes should implement the following
  172. methods:
  173. * ``__init__(self, *args, **kwargs)``
  174. * ``transitions(self, year)`` - this is expected to return a tuple of
  175. datetimes representing the DST on and off transitions in standard
  176. time.
  177. A fully initialized ``tzrangebase`` subclass should also provide the
  178. following attributes:
  179. * ``hasdst``: Boolean whether or not the zone uses DST.
  180. * ``_dst_offset`` / ``_std_offset``: :class:`datetime.timedelta` objects
  181. representing the respective UTC offsets.
  182. * ``_dst_abbr`` / ``_std_abbr``: Strings representing the timezone short
  183. abbreviations in DST and STD, respectively.
  184. * ``_hasdst``: Whether or not the zone has DST.
  185. ..versionadded:: 2.6.0
  186. """
  187. def __init__(self):
  188. raise NotImplementedError('tzrangebase is an abstract base class')
  189. def utcoffset(self, dt):
  190. isdst = self._isdst(dt)
  191. if isdst is None:
  192. return None
  193. elif isdst:
  194. return self._dst_offset
  195. else:
  196. return self._std_offset
  197. def dst(self, dt):
  198. isdst = self._isdst(dt)
  199. if isdst is None:
  200. return None
  201. elif isdst:
  202. return self._dst_base_offset
  203. else:
  204. return ZERO
  205. @tzname_in_python2
  206. def tzname(self, dt):
  207. if self._isdst(dt):
  208. return self._dst_abbr
  209. else:
  210. return self._std_abbr
  211. def fromutc(self, dt):
  212. """ Given a datetime in UTC, return local time """
  213. if not isinstance(dt, datetime):
  214. raise TypeError("fromutc() requires a datetime argument")
  215. if dt.tzinfo is not self:
  216. raise ValueError("dt.tzinfo is not self")
  217. # Get transitions - if there are none, fixed offset
  218. transitions = self.transitions(dt.year)
  219. if transitions is None:
  220. return dt + self.utcoffset(dt)
  221. # Get the transition times in UTC
  222. dston, dstoff = transitions
  223. dston -= self._std_offset
  224. dstoff -= self._std_offset
  225. utc_transitions = (dston, dstoff)
  226. dt_utc = dt.replace(tzinfo=None)
  227. isdst = self._naive_isdst(dt_utc, utc_transitions)
  228. if isdst:
  229. dt_wall = dt + self._dst_offset
  230. else:
  231. dt_wall = dt + self._std_offset
  232. _fold = int(not isdst and self.is_ambiguous(dt_wall))
  233. return enfold(dt_wall, fold=_fold)
  234. def is_ambiguous(self, dt):
  235. """
  236. Whether or not the "wall time" of a given datetime is ambiguous in this
  237. zone.
  238. :param dt:
  239. A :py:class:`datetime.datetime`, naive or time zone aware.
  240. :return:
  241. Returns ``True`` if ambiguous, ``False`` otherwise.
  242. .. versionadded:: 2.6.0
  243. """
  244. if not self.hasdst:
  245. return False
  246. start, end = self.transitions(dt.year)
  247. dt = dt.replace(tzinfo=None)
  248. return (end <= dt < end + self._dst_base_offset)
  249. def _isdst(self, dt):
  250. if not self.hasdst:
  251. return False
  252. elif dt is None:
  253. return None
  254. transitions = self.transitions(dt.year)
  255. if transitions is None:
  256. return False
  257. dt = dt.replace(tzinfo=None)
  258. isdst = self._naive_isdst(dt, transitions)
  259. # Handle ambiguous dates
  260. if not isdst and self.is_ambiguous(dt):
  261. return not self._fold(dt)
  262. else:
  263. return isdst
  264. def _naive_isdst(self, dt, transitions):
  265. dston, dstoff = transitions
  266. dt = dt.replace(tzinfo=None)
  267. if dston < dstoff:
  268. isdst = dston <= dt < dstoff
  269. else:
  270. isdst = not dstoff <= dt < dston
  271. return isdst
  272. @property
  273. def _dst_base_offset(self):
  274. return self._dst_offset - self._std_offset
  275. __hash__ = None
  276. def __ne__(self, other):
  277. return not (self == other)
  278. def __repr__(self):
  279. return "%s(...)" % self.__class__.__name__
  280. __reduce__ = object.__reduce__
  281. def _total_seconds(td):
  282. # Python 2.6 doesn't have a total_seconds() method on timedelta objects
  283. return ((td.seconds + td.days * 86400) * 1000000 +
  284. td.microseconds) // 1000000
  285. _total_seconds = getattr(timedelta, 'total_seconds', _total_seconds)