You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

420 line
13 KiB

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