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.

tzinfo.py 19KB

5 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  1. '''Base classes and helpers for building zone specific tzinfo classes'''
  2. from datetime import datetime, timedelta, tzinfo
  3. from bisect import bisect_right
  4. try:
  5. set
  6. except NameError:
  7. from sets import Set as set
  8. import pytz
  9. from pytz.exceptions import AmbiguousTimeError, NonExistentTimeError
  10. __all__ = []
  11. _timedelta_cache = {}
  12. def memorized_timedelta(seconds):
  13. '''Create only one instance of each distinct timedelta'''
  14. try:
  15. return _timedelta_cache[seconds]
  16. except KeyError:
  17. delta = timedelta(seconds=seconds)
  18. _timedelta_cache[seconds] = delta
  19. return delta
  20. _epoch = datetime.utcfromtimestamp(0)
  21. _datetime_cache = {0: _epoch}
  22. def memorized_datetime(seconds):
  23. '''Create only one instance of each distinct datetime'''
  24. try:
  25. return _datetime_cache[seconds]
  26. except KeyError:
  27. # NB. We can't just do datetime.utcfromtimestamp(seconds) as this
  28. # fails with negative values under Windows (Bug #90096)
  29. dt = _epoch + timedelta(seconds=seconds)
  30. _datetime_cache[seconds] = dt
  31. return dt
  32. _ttinfo_cache = {}
  33. def memorized_ttinfo(*args):
  34. '''Create only one instance of each distinct tuple'''
  35. try:
  36. return _ttinfo_cache[args]
  37. except KeyError:
  38. ttinfo = (
  39. memorized_timedelta(args[0]),
  40. memorized_timedelta(args[1]),
  41. args[2]
  42. )
  43. _ttinfo_cache[args] = ttinfo
  44. return ttinfo
  45. _notime = memorized_timedelta(0)
  46. def _to_seconds(td):
  47. '''Convert a timedelta to seconds'''
  48. return td.seconds + td.days * 24 * 60 * 60
  49. class BaseTzInfo(tzinfo):
  50. # Overridden in subclass
  51. _utcoffset = None
  52. _tzname = None
  53. zone = None
  54. def __str__(self):
  55. return self.zone
  56. class StaticTzInfo(BaseTzInfo):
  57. '''A timezone that has a constant offset from UTC
  58. These timezones are rare, as most locations have changed their
  59. offset at some point in their history
  60. '''
  61. def fromutc(self, dt):
  62. '''See datetime.tzinfo.fromutc'''
  63. if dt.tzinfo is not None and dt.tzinfo is not self:
  64. raise ValueError('fromutc: dt.tzinfo is not self')
  65. return (dt + self._utcoffset).replace(tzinfo=self)
  66. def utcoffset(self, dt, is_dst=None):
  67. '''See datetime.tzinfo.utcoffset
  68. is_dst is ignored for StaticTzInfo, and exists only to
  69. retain compatibility with DstTzInfo.
  70. '''
  71. return self._utcoffset
  72. def dst(self, dt, is_dst=None):
  73. '''See datetime.tzinfo.dst
  74. is_dst is ignored for StaticTzInfo, and exists only to
  75. retain compatibility with DstTzInfo.
  76. '''
  77. return _notime
  78. def tzname(self, dt, is_dst=None):
  79. '''See datetime.tzinfo.tzname
  80. is_dst is ignored for StaticTzInfo, and exists only to
  81. retain compatibility with DstTzInfo.
  82. '''
  83. return self._tzname
  84. def localize(self, dt, is_dst=False):
  85. '''Convert naive time to local time'''
  86. if dt.tzinfo is not None:
  87. raise ValueError('Not naive datetime (tzinfo is already set)')
  88. return dt.replace(tzinfo=self)
  89. def normalize(self, dt, is_dst=False):
  90. '''Correct the timezone information on the given datetime.
  91. This is normally a no-op, as StaticTzInfo timezones never have
  92. ambiguous cases to correct:
  93. >>> from pytz import timezone
  94. >>> gmt = timezone('GMT')
  95. >>> isinstance(gmt, StaticTzInfo)
  96. True
  97. >>> dt = datetime(2011, 5, 8, 1, 2, 3, tzinfo=gmt)
  98. >>> gmt.normalize(dt) is dt
  99. True
  100. The supported method of converting between timezones is to use
  101. datetime.astimezone(). Currently normalize() also works:
  102. >>> la = timezone('America/Los_Angeles')
  103. >>> dt = la.localize(datetime(2011, 5, 7, 1, 2, 3))
  104. >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)'
  105. >>> gmt.normalize(dt).strftime(fmt)
  106. '2011-05-07 08:02:03 GMT (+0000)'
  107. '''
  108. if dt.tzinfo is self:
  109. return dt
  110. if dt.tzinfo is None:
  111. raise ValueError('Naive time - no tzinfo set')
  112. return dt.astimezone(self)
  113. def __repr__(self):
  114. return '<StaticTzInfo %r>' % (self.zone,)
  115. def __reduce__(self):
  116. # Special pickle to zone remains a singleton and to cope with
  117. # database changes.
  118. return pytz._p, (self.zone,)
  119. class DstTzInfo(BaseTzInfo):
  120. '''A timezone that has a variable offset from UTC
  121. The offset might change if daylight saving time comes into effect,
  122. or at a point in history when the region decides to change their
  123. timezone definition.
  124. '''
  125. # Overridden in subclass
  126. # Sorted list of DST transition times, UTC
  127. _utc_transition_times = None
  128. # [(utcoffset, dstoffset, tzname)] corresponding to
  129. # _utc_transition_times entries
  130. _transition_info = None
  131. zone = None
  132. # Set in __init__
  133. _tzinfos = None
  134. _dst = None # DST offset
  135. def __init__(self, _inf=None, _tzinfos=None):
  136. if _inf:
  137. self._tzinfos = _tzinfos
  138. self._utcoffset, self._dst, self._tzname = _inf
  139. else:
  140. _tzinfos = {}
  141. self._tzinfos = _tzinfos
  142. self._utcoffset, self._dst, self._tzname = (
  143. self._transition_info[0])
  144. _tzinfos[self._transition_info[0]] = self
  145. for inf in self._transition_info[1:]:
  146. if inf not in _tzinfos:
  147. _tzinfos[inf] = self.__class__(inf, _tzinfos)
  148. def fromutc(self, dt):
  149. '''See datetime.tzinfo.fromutc'''
  150. if (dt.tzinfo is not None and
  151. getattr(dt.tzinfo, '_tzinfos', None) is not self._tzinfos):
  152. raise ValueError('fromutc: dt.tzinfo is not self')
  153. dt = dt.replace(tzinfo=None)
  154. idx = max(0, bisect_right(self._utc_transition_times, dt) - 1)
  155. inf = self._transition_info[idx]
  156. return (dt + inf[0]).replace(tzinfo=self._tzinfos[inf])
  157. def normalize(self, dt):
  158. '''Correct the timezone information on the given datetime
  159. If date arithmetic crosses DST boundaries, the tzinfo
  160. is not magically adjusted. This method normalizes the
  161. tzinfo to the correct one.
  162. To test, first we need to do some setup
  163. >>> from pytz import timezone
  164. >>> utc = timezone('UTC')
  165. >>> eastern = timezone('US/Eastern')
  166. >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)'
  167. We next create a datetime right on an end-of-DST transition point,
  168. the instant when the wallclocks are wound back one hour.
  169. >>> utc_dt = datetime(2002, 10, 27, 6, 0, 0, tzinfo=utc)
  170. >>> loc_dt = utc_dt.astimezone(eastern)
  171. >>> loc_dt.strftime(fmt)
  172. '2002-10-27 01:00:00 EST (-0500)'
  173. Now, if we subtract a few minutes from it, note that the timezone
  174. information has not changed.
  175. >>> before = loc_dt - timedelta(minutes=10)
  176. >>> before.strftime(fmt)
  177. '2002-10-27 00:50:00 EST (-0500)'
  178. But we can fix that by calling the normalize method
  179. >>> before = eastern.normalize(before)
  180. >>> before.strftime(fmt)
  181. '2002-10-27 01:50:00 EDT (-0400)'
  182. The supported method of converting between timezones is to use
  183. datetime.astimezone(). Currently, normalize() also works:
  184. >>> th = timezone('Asia/Bangkok')
  185. >>> am = timezone('Europe/Amsterdam')
  186. >>> dt = th.localize(datetime(2011, 5, 7, 1, 2, 3))
  187. >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)'
  188. >>> am.normalize(dt).strftime(fmt)
  189. '2011-05-06 20:02:03 CEST (+0200)'
  190. '''
  191. if dt.tzinfo is None:
  192. raise ValueError('Naive time - no tzinfo set')
  193. # Convert dt in localtime to UTC
  194. offset = dt.tzinfo._utcoffset
  195. dt = dt.replace(tzinfo=None)
  196. dt = dt - offset
  197. # convert it back, and return it
  198. return self.fromutc(dt)
  199. def localize(self, dt, is_dst=False):
  200. '''Convert naive time to local time.
  201. This method should be used to construct localtimes, rather
  202. than passing a tzinfo argument to a datetime constructor.
  203. is_dst is used to determine the correct timezone in the ambigous
  204. period at the end of daylight saving time.
  205. >>> from pytz import timezone
  206. >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)'
  207. >>> amdam = timezone('Europe/Amsterdam')
  208. >>> dt = datetime(2004, 10, 31, 2, 0, 0)
  209. >>> loc_dt1 = amdam.localize(dt, is_dst=True)
  210. >>> loc_dt2 = amdam.localize(dt, is_dst=False)
  211. >>> loc_dt1.strftime(fmt)
  212. '2004-10-31 02:00:00 CEST (+0200)'
  213. >>> loc_dt2.strftime(fmt)
  214. '2004-10-31 02:00:00 CET (+0100)'
  215. >>> str(loc_dt2 - loc_dt1)
  216. '1:00:00'
  217. Use is_dst=None to raise an AmbiguousTimeError for ambiguous
  218. times at the end of daylight saving time
  219. >>> try:
  220. ... loc_dt1 = amdam.localize(dt, is_dst=None)
  221. ... except AmbiguousTimeError:
  222. ... print('Ambiguous')
  223. Ambiguous
  224. is_dst defaults to False
  225. >>> amdam.localize(dt) == amdam.localize(dt, False)
  226. True
  227. is_dst is also used to determine the correct timezone in the
  228. wallclock times jumped over at the start of daylight saving time.
  229. >>> pacific = timezone('US/Pacific')
  230. >>> dt = datetime(2008, 3, 9, 2, 0, 0)
  231. >>> ploc_dt1 = pacific.localize(dt, is_dst=True)
  232. >>> ploc_dt2 = pacific.localize(dt, is_dst=False)
  233. >>> ploc_dt1.strftime(fmt)
  234. '2008-03-09 02:00:00 PDT (-0700)'
  235. >>> ploc_dt2.strftime(fmt)
  236. '2008-03-09 02:00:00 PST (-0800)'
  237. >>> str(ploc_dt2 - ploc_dt1)
  238. '1:00:00'
  239. Use is_dst=None to raise a NonExistentTimeError for these skipped
  240. times.
  241. >>> try:
  242. ... loc_dt1 = pacific.localize(dt, is_dst=None)
  243. ... except NonExistentTimeError:
  244. ... print('Non-existent')
  245. Non-existent
  246. '''
  247. if dt.tzinfo is not None:
  248. raise ValueError('Not naive datetime (tzinfo is already set)')
  249. # Find the two best possibilities.
  250. possible_loc_dt = set()
  251. for delta in [timedelta(days=-1), timedelta(days=1)]:
  252. loc_dt = dt + delta
  253. idx = max(0, bisect_right(
  254. self._utc_transition_times, loc_dt) - 1)
  255. inf = self._transition_info[idx]
  256. tzinfo = self._tzinfos[inf]
  257. loc_dt = tzinfo.normalize(dt.replace(tzinfo=tzinfo))
  258. if loc_dt.replace(tzinfo=None) == dt:
  259. possible_loc_dt.add(loc_dt)
  260. if len(possible_loc_dt) == 1:
  261. return possible_loc_dt.pop()
  262. # If there are no possibly correct timezones, we are attempting
  263. # to convert a time that never happened - the time period jumped
  264. # during the start-of-DST transition period.
  265. if len(possible_loc_dt) == 0:
  266. # If we refuse to guess, raise an exception.
  267. if is_dst is None:
  268. raise NonExistentTimeError(dt)
  269. # If we are forcing the pre-DST side of the DST transition, we
  270. # obtain the correct timezone by winding the clock forward a few
  271. # hours.
  272. elif is_dst:
  273. return self.localize(
  274. dt + timedelta(hours=6), is_dst=True) - timedelta(hours=6)
  275. # If we are forcing the post-DST side of the DST transition, we
  276. # obtain the correct timezone by winding the clock back.
  277. else:
  278. return self.localize(
  279. dt - timedelta(hours=6),
  280. is_dst=False) + timedelta(hours=6)
  281. # If we get this far, we have multiple possible timezones - this
  282. # is an ambiguous case occuring during the end-of-DST transition.
  283. # If told to be strict, raise an exception since we have an
  284. # ambiguous case
  285. if is_dst is None:
  286. raise AmbiguousTimeError(dt)
  287. # Filter out the possiblilities that don't match the requested
  288. # is_dst
  289. filtered_possible_loc_dt = [
  290. p for p in possible_loc_dt if bool(p.tzinfo._dst) == is_dst
  291. ]
  292. # Hopefully we only have one possibility left. Return it.
  293. if len(filtered_possible_loc_dt) == 1:
  294. return filtered_possible_loc_dt[0]
  295. if len(filtered_possible_loc_dt) == 0:
  296. filtered_possible_loc_dt = list(possible_loc_dt)
  297. # If we get this far, we have in a wierd timezone transition
  298. # where the clocks have been wound back but is_dst is the same
  299. # in both (eg. Europe/Warsaw 1915 when they switched to CET).
  300. # At this point, we just have to guess unless we allow more
  301. # hints to be passed in (such as the UTC offset or abbreviation),
  302. # but that is just getting silly.
  303. #
  304. # Choose the earliest (by UTC) applicable timezone if is_dst=True
  305. # Choose the latest (by UTC) applicable timezone if is_dst=False
  306. # i.e., behave like end-of-DST transition
  307. dates = {} # utc -> local
  308. for local_dt in filtered_possible_loc_dt:
  309. utc_time = (
  310. local_dt.replace(tzinfo=None) - local_dt.tzinfo._utcoffset)
  311. assert utc_time not in dates
  312. dates[utc_time] = local_dt
  313. return dates[[min, max][not is_dst](dates)]
  314. def utcoffset(self, dt, is_dst=None):
  315. '''See datetime.tzinfo.utcoffset
  316. The is_dst parameter may be used to remove ambiguity during DST
  317. transitions.
  318. >>> from pytz import timezone
  319. >>> tz = timezone('America/St_Johns')
  320. >>> ambiguous = datetime(2009, 10, 31, 23, 30)
  321. >>> str(tz.utcoffset(ambiguous, is_dst=False))
  322. '-1 day, 20:30:00'
  323. >>> str(tz.utcoffset(ambiguous, is_dst=True))
  324. '-1 day, 21:30:00'
  325. >>> try:
  326. ... tz.utcoffset(ambiguous)
  327. ... except AmbiguousTimeError:
  328. ... print('Ambiguous')
  329. Ambiguous
  330. '''
  331. if dt is None:
  332. return None
  333. elif dt.tzinfo is not self:
  334. dt = self.localize(dt, is_dst)
  335. return dt.tzinfo._utcoffset
  336. else:
  337. return self._utcoffset
  338. def dst(self, dt, is_dst=None):
  339. '''See datetime.tzinfo.dst
  340. The is_dst parameter may be used to remove ambiguity during DST
  341. transitions.
  342. >>> from pytz import timezone
  343. >>> tz = timezone('America/St_Johns')
  344. >>> normal = datetime(2009, 9, 1)
  345. >>> str(tz.dst(normal))
  346. '1:00:00'
  347. >>> str(tz.dst(normal, is_dst=False))
  348. '1:00:00'
  349. >>> str(tz.dst(normal, is_dst=True))
  350. '1:00:00'
  351. >>> ambiguous = datetime(2009, 10, 31, 23, 30)
  352. >>> str(tz.dst(ambiguous, is_dst=False))
  353. '0:00:00'
  354. >>> str(tz.dst(ambiguous, is_dst=True))
  355. '1:00:00'
  356. >>> try:
  357. ... tz.dst(ambiguous)
  358. ... except AmbiguousTimeError:
  359. ... print('Ambiguous')
  360. Ambiguous
  361. '''
  362. if dt is None:
  363. return None
  364. elif dt.tzinfo is not self:
  365. dt = self.localize(dt, is_dst)
  366. return dt.tzinfo._dst
  367. else:
  368. return self._dst
  369. def tzname(self, dt, is_dst=None):
  370. '''See datetime.tzinfo.tzname
  371. The is_dst parameter may be used to remove ambiguity during DST
  372. transitions.
  373. >>> from pytz import timezone
  374. >>> tz = timezone('America/St_Johns')
  375. >>> normal = datetime(2009, 9, 1)
  376. >>> tz.tzname(normal)
  377. 'NDT'
  378. >>> tz.tzname(normal, is_dst=False)
  379. 'NDT'
  380. >>> tz.tzname(normal, is_dst=True)
  381. 'NDT'
  382. >>> ambiguous = datetime(2009, 10, 31, 23, 30)
  383. >>> tz.tzname(ambiguous, is_dst=False)
  384. 'NST'
  385. >>> tz.tzname(ambiguous, is_dst=True)
  386. 'NDT'
  387. >>> try:
  388. ... tz.tzname(ambiguous)
  389. ... except AmbiguousTimeError:
  390. ... print('Ambiguous')
  391. Ambiguous
  392. '''
  393. if dt is None:
  394. return self.zone
  395. elif dt.tzinfo is not self:
  396. dt = self.localize(dt, is_dst)
  397. return dt.tzinfo._tzname
  398. else:
  399. return self._tzname
  400. def __repr__(self):
  401. if self._dst:
  402. dst = 'DST'
  403. else:
  404. dst = 'STD'
  405. if self._utcoffset > _notime:
  406. return '<DstTzInfo %r %s+%s %s>' % (
  407. self.zone, self._tzname, self._utcoffset, dst
  408. )
  409. else:
  410. return '<DstTzInfo %r %s%s %s>' % (
  411. self.zone, self._tzname, self._utcoffset, dst
  412. )
  413. def __reduce__(self):
  414. # Special pickle to zone remains a singleton and to cope with
  415. # database changes.
  416. return pytz._p, (
  417. self.zone,
  418. _to_seconds(self._utcoffset),
  419. _to_seconds(self._dst),
  420. self._tzname
  421. )
  422. def unpickler(zone, utcoffset=None, dstoffset=None, tzname=None):
  423. """Factory function for unpickling pytz tzinfo instances.
  424. This is shared for both StaticTzInfo and DstTzInfo instances, because
  425. database changes could cause a zones implementation to switch between
  426. these two base classes and we can't break pickles on a pytz version
  427. upgrade.
  428. """
  429. # Raises a KeyError if zone no longer exists, which should never happen
  430. # and would be a bug.
  431. tz = pytz.timezone(zone)
  432. # A StaticTzInfo - just return it
  433. if utcoffset is None:
  434. return tz
  435. # This pickle was created from a DstTzInfo. We need to
  436. # determine which of the list of tzinfo instances for this zone
  437. # to use in order to restore the state of any datetime instances using
  438. # it correctly.
  439. utcoffset = memorized_timedelta(utcoffset)
  440. dstoffset = memorized_timedelta(dstoffset)
  441. try:
  442. return tz._tzinfos[(utcoffset, dstoffset, tzname)]
  443. except KeyError:
  444. # The particular state requested in this timezone no longer exists.
  445. # This indicates a corrupt pickle, or the timezone database has been
  446. # corrected violently enough to make this particular
  447. # (utcoffset,dstoffset) no longer exist in the zone, or the
  448. # abbreviation has been changed.
  449. pass
  450. # See if we can find an entry differing only by tzname. Abbreviations
  451. # get changed from the initial guess by the database maintainers to
  452. # match reality when this information is discovered.
  453. for localized_tz in tz._tzinfos.values():
  454. if (localized_tz._utcoffset == utcoffset and
  455. localized_tz._dst == dstoffset):
  456. return localized_tz
  457. # This (utcoffset, dstoffset) information has been removed from the
  458. # zone. Add it back. This might occur when the database maintainers have
  459. # corrected incorrect information. datetime instances using this
  460. # incorrect information will continue to do so, exactly as they were
  461. # before being pickled. This is purely an overly paranoid safety net - I
  462. # doubt this will ever been needed in real life.
  463. inf = (utcoffset, dstoffset, tzname)
  464. tz._tzinfos[inf] = tz.__class__(inf, tz._tzinfos)
  465. return tz._tzinfos[inf]