tzinfo.py 19 KB

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