Development of an internal social media platform with personalised dashboards for students
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.

_common.py 13KB

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