tzinfo.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585
  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(1970, 1, 1, 0, 0) # 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.fromtimestamp(seconds, tz=timezone.utc).replace(tzinfo=None)
  28. # as this 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. try:
  253. loc_dt = dt + delta
  254. except OverflowError:
  255. # dt is close to datetime.min or datetime.max; skip this
  256. # direction rather than raising an OverflowError to the caller.
  257. continue
  258. idx = max(0, bisect_right(
  259. self._utc_transition_times, loc_dt) - 1)
  260. inf = self._transition_info[idx]
  261. tzinfo = self._tzinfos[inf]
  262. loc_dt = tzinfo.normalize(dt.replace(tzinfo=tzinfo))
  263. if loc_dt.replace(tzinfo=None) == dt:
  264. possible_loc_dt.add(loc_dt)
  265. if len(possible_loc_dt) == 1:
  266. return possible_loc_dt.pop()
  267. # If there are no possibly correct timezones, we are attempting
  268. # to convert a time that never happened - the time period jumped
  269. # during the start-of-DST transition period.
  270. if len(possible_loc_dt) == 0:
  271. # If we refuse to guess, raise an exception.
  272. if is_dst is None:
  273. raise NonExistentTimeError(dt)
  274. # If we are forcing the pre-DST side of the DST transition, we
  275. # obtain the correct timezone by winding the clock forward a few
  276. # hours.
  277. elif is_dst:
  278. return self.localize(
  279. dt + timedelta(hours=6), is_dst=True) - timedelta(hours=6)
  280. # If we are forcing the post-DST side of the DST transition, we
  281. # obtain the correct timezone by winding the clock back.
  282. else:
  283. return self.localize(
  284. dt - timedelta(hours=6),
  285. is_dst=False) + timedelta(hours=6)
  286. # If we get this far, we have multiple possible timezones - this
  287. # is an ambiguous case occurring during the end-of-DST transition.
  288. # If told to be strict, raise an exception since we have an
  289. # ambiguous case
  290. if is_dst is None:
  291. raise AmbiguousTimeError(dt)
  292. # Filter out the possiblilities that don't match the requested
  293. # is_dst
  294. filtered_possible_loc_dt = [
  295. p for p in possible_loc_dt if bool(p.tzinfo._dst) == is_dst
  296. ]
  297. # Hopefully we only have one possibility left. Return it.
  298. if len(filtered_possible_loc_dt) == 1:
  299. return filtered_possible_loc_dt[0]
  300. if len(filtered_possible_loc_dt) == 0:
  301. filtered_possible_loc_dt = list(possible_loc_dt)
  302. # If we get this far, we have in a wierd timezone transition
  303. # where the clocks have been wound back but is_dst is the same
  304. # in both (eg. Europe/Warsaw 1915 when they switched to CET).
  305. # At this point, we just have to guess unless we allow more
  306. # hints to be passed in (such as the UTC offset or abbreviation),
  307. # but that is just getting silly.
  308. #
  309. # Choose the earliest (by UTC) applicable timezone if is_dst=True
  310. # Choose the latest (by UTC) applicable timezone if is_dst=False
  311. # i.e., behave like end-of-DST transition
  312. dates = {} # utc -> local
  313. for local_dt in filtered_possible_loc_dt:
  314. utc_time = (
  315. local_dt.replace(tzinfo=None) - local_dt.tzinfo._utcoffset)
  316. assert utc_time not in dates
  317. dates[utc_time] = local_dt
  318. return dates[[min, max][not is_dst](dates)]
  319. def utcoffset(self, dt, is_dst=None):
  320. '''See datetime.tzinfo.utcoffset
  321. The is_dst parameter may be used to remove ambiguity during DST
  322. transitions.
  323. >>> from pytz import timezone
  324. >>> tz = timezone('America/St_Johns')
  325. >>> ambiguous = datetime(2009, 10, 31, 23, 30)
  326. >>> str(tz.utcoffset(ambiguous, is_dst=False))
  327. '-1 day, 20:30:00'
  328. >>> str(tz.utcoffset(ambiguous, is_dst=True))
  329. '-1 day, 21:30:00'
  330. >>> try:
  331. ... tz.utcoffset(ambiguous)
  332. ... except AmbiguousTimeError:
  333. ... print('Ambiguous')
  334. Ambiguous
  335. '''
  336. if dt is None:
  337. return None
  338. elif dt.tzinfo is not self:
  339. dt = self.localize(dt, is_dst)
  340. return dt.tzinfo._utcoffset
  341. else:
  342. return self._utcoffset
  343. def dst(self, dt, is_dst=None):
  344. '''See datetime.tzinfo.dst
  345. The is_dst parameter may be used to remove ambiguity during DST
  346. transitions.
  347. >>> from pytz import timezone
  348. >>> tz = timezone('America/St_Johns')
  349. >>> normal = datetime(2009, 9, 1)
  350. >>> str(tz.dst(normal))
  351. '1:00:00'
  352. >>> str(tz.dst(normal, is_dst=False))
  353. '1:00:00'
  354. >>> str(tz.dst(normal, is_dst=True))
  355. '1:00:00'
  356. >>> ambiguous = datetime(2009, 10, 31, 23, 30)
  357. >>> str(tz.dst(ambiguous, is_dst=False))
  358. '0:00:00'
  359. >>> str(tz.dst(ambiguous, is_dst=True))
  360. '1:00:00'
  361. >>> try:
  362. ... tz.dst(ambiguous)
  363. ... except AmbiguousTimeError:
  364. ... print('Ambiguous')
  365. Ambiguous
  366. '''
  367. if dt is None:
  368. return None
  369. elif dt.tzinfo is not self:
  370. dt = self.localize(dt, is_dst)
  371. return dt.tzinfo._dst
  372. else:
  373. return self._dst
  374. def tzname(self, dt, is_dst=None):
  375. '''See datetime.tzinfo.tzname
  376. The is_dst parameter may be used to remove ambiguity during DST
  377. transitions.
  378. >>> from pytz import timezone
  379. >>> tz = timezone('America/St_Johns')
  380. >>> normal = datetime(2009, 9, 1)
  381. >>> tz.tzname(normal)
  382. 'NDT'
  383. >>> tz.tzname(normal, is_dst=False)
  384. 'NDT'
  385. >>> tz.tzname(normal, is_dst=True)
  386. 'NDT'
  387. >>> ambiguous = datetime(2009, 10, 31, 23, 30)
  388. >>> tz.tzname(ambiguous, is_dst=False)
  389. 'NST'
  390. >>> tz.tzname(ambiguous, is_dst=True)
  391. 'NDT'
  392. >>> try:
  393. ... tz.tzname(ambiguous)
  394. ... except AmbiguousTimeError:
  395. ... print('Ambiguous')
  396. Ambiguous
  397. '''
  398. if dt is None:
  399. return self.zone
  400. elif dt.tzinfo is not self:
  401. dt = self.localize(dt, is_dst)
  402. return dt.tzinfo._tzname
  403. else:
  404. return self._tzname
  405. def __repr__(self):
  406. if self._dst:
  407. dst = 'DST'
  408. else:
  409. dst = 'STD'
  410. if self._utcoffset > _notime:
  411. return '<DstTzInfo %r %s+%s %s>' % (
  412. self.zone, self._tzname, self._utcoffset, dst
  413. )
  414. else:
  415. return '<DstTzInfo %r %s%s %s>' % (
  416. self.zone, self._tzname, self._utcoffset, dst
  417. )
  418. def __reduce__(self):
  419. # Special pickle to zone remains a singleton and to cope with
  420. # database changes.
  421. return pytz._p, (
  422. self.zone,
  423. _to_seconds(self._utcoffset),
  424. _to_seconds(self._dst),
  425. self._tzname
  426. )
  427. def unpickler(zone, utcoffset=None, dstoffset=None, tzname=None):
  428. """Factory function for unpickling pytz tzinfo instances.
  429. This is shared for both StaticTzInfo and DstTzInfo instances, because
  430. database changes could cause a zones implementation to switch between
  431. these two base classes and we can't break pickles on a pytz version
  432. upgrade.
  433. """
  434. # Raises a KeyError if zone no longer exists, which should never happen
  435. # and would be a bug.
  436. tz = pytz.timezone(zone)
  437. # A StaticTzInfo - just return it
  438. if utcoffset is None:
  439. return tz
  440. # This pickle was created from a DstTzInfo. We need to
  441. # determine which of the list of tzinfo instances for this zone
  442. # to use in order to restore the state of any datetime instances using
  443. # it correctly.
  444. utcoffset = memorized_timedelta(utcoffset)
  445. dstoffset = memorized_timedelta(dstoffset)
  446. try:
  447. return tz._tzinfos[(utcoffset, dstoffset, tzname)]
  448. except KeyError:
  449. # The particular state requested in this timezone no longer exists.
  450. # This indicates a corrupt pickle, or the timezone database has been
  451. # corrected violently enough to make this particular
  452. # (utcoffset,dstoffset) no longer exist in the zone, or the
  453. # abbreviation has been changed.
  454. pass
  455. # See if we can find an entry differing only by tzname. Abbreviations
  456. # get changed from the initial guess by the database maintainers to
  457. # match reality when this information is discovered.
  458. for localized_tz in tz._tzinfos.values():
  459. if (localized_tz._utcoffset == utcoffset and
  460. localized_tz._dst == dstoffset):
  461. return localized_tz
  462. # This (utcoffset, dstoffset) information has been removed from the
  463. # zone. Add it back. This might occur when the database maintainers have
  464. # corrected incorrect information. datetime instances using this
  465. # incorrect information will continue to do so, exactly as they were
  466. # before being pickled. This is purely an overly paranoid safety net - I
  467. # doubt this will ever been needed in real life.
  468. inf = (utcoffset, dstoffset, tzname)
  469. tz._tzinfos[inf] = tz.__class__(inf, tz._tzinfos)
  470. return tz._tzinfos[inf]