codekingpro/portable-devtools
114k
1'''Base classes and helpers for building zone specific tzinfo classes'''2 3from datetime import datetime, timedelta, tzinfo4from bisect import bisect_right5try:6 set7except NameError:8 from sets import Set as set9 10import pytz11from pytz.exceptions import AmbiguousTimeError, NonExistentTimeError12 13__all__ = []14 15_timedelta_cache = {}16 17 18def memorized_timedelta(seconds):19 '''Create only one instance of each distinct timedelta'''20 try:21 return _timedelta_cache[seconds]22 except KeyError:23 delta = timedelta(seconds=seconds)24 _timedelta_cache[seconds] = delta25 return delta26 27 28_epoch = datetime(1970, 1, 1, 0, 0) # datetime.utcfromtimestamp(0)29_datetime_cache = {0: _epoch}30 31 32def memorized_datetime(seconds):33 '''Create only one instance of each distinct datetime'''34 try:35 return _datetime_cache[seconds]36 except KeyError:37 # NB. We can't just do datetime.fromtimestamp(seconds, tz=timezone.utc).replace(tzinfo=None)38 # as this fails with negative values under Windows (Bug #90096)39 dt = _epoch + timedelta(seconds=seconds)40 _datetime_cache[seconds] = dt41 return dt42 43 44_ttinfo_cache = {}45 46 47def memorized_ttinfo(*args):48 '''Create only one instance of each distinct tuple'''49 try:50 return _ttinfo_cache[args]51 except KeyError:52 ttinfo = (53 memorized_timedelta(args[0]),54 memorized_timedelta(args[1]),55 args[2]56 )57 _ttinfo_cache[args] = ttinfo58 return ttinfo59 60 61_notime = memorized_timedelta(0)62 63 64def _to_seconds(td):65 '''Convert a timedelta to seconds'''66 return td.seconds + td.days * 24 * 60 * 6067 68 69class BaseTzInfo(tzinfo):70 # Overridden in subclass71 _utcoffset = None72 _tzname = None73 zone = None74 75 def __str__(self):76 return self.zone77 78 79class StaticTzInfo(BaseTzInfo):80 '''A timezone that has a constant offset from UTC81 82 These timezones are rare, as most locations have changed their83 offset at some point in their history84 '''85 def fromutc(self, dt):86 '''See datetime.tzinfo.fromutc'''87 if dt.tzinfo is not None and dt.tzinfo is not self:88 raise ValueError('fromutc: dt.tzinfo is not self')89 return (dt + self._utcoffset).replace(tzinfo=self)90 91 def utcoffset(self, dt, is_dst=None):92 '''See datetime.tzinfo.utcoffset93 94 is_dst is ignored for StaticTzInfo, and exists only to95 retain compatibility with DstTzInfo.96 '''97 return self._utcoffset98 99 def dst(self, dt, is_dst=None):100 '''See datetime.tzinfo.dst101 102 is_dst is ignored for StaticTzInfo, and exists only to103 retain compatibility with DstTzInfo.104 '''105 return _notime106 107 def tzname(self, dt, is_dst=None):108 '''See datetime.tzinfo.tzname109 110 is_dst is ignored for StaticTzInfo, and exists only to111 retain compatibility with DstTzInfo.112 '''113 return self._tzname114 115 def localize(self, dt, is_dst=False):116 '''Convert naive time to local time'''117 if dt.tzinfo is not None:118 raise ValueError('Not naive datetime (tzinfo is already set)')119 return dt.replace(tzinfo=self)120 121 def normalize(self, dt, is_dst=False):122 '''Correct the timezone information on the given datetime.123 124 This is normally a no-op, as StaticTzInfo timezones never have125 ambiguous cases to correct:126 127 >>> from pytz import timezone128 >>> gmt = timezone('GMT')129 >>> isinstance(gmt, StaticTzInfo)130 True131 >>> dt = datetime(2011, 5, 8, 1, 2, 3, tzinfo=gmt)132 >>> gmt.normalize(dt) is dt133 True134 135 The supported method of converting between timezones is to use136 datetime.astimezone(). Currently normalize() also works:137 138 >>> la = timezone('America/Los_Angeles')139 >>> dt = la.localize(datetime(2011, 5, 7, 1, 2, 3))140 >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)'141 >>> gmt.normalize(dt).strftime(fmt)142 '2011-05-07 08:02:03 GMT (+0000)'143 '''144 if dt.tzinfo is self:145 return dt146 if dt.tzinfo is None:147 raise ValueError('Naive time - no tzinfo set')148 return dt.astimezone(self)149 150 def __repr__(self):151 return '<StaticTzInfo %r>' % (self.zone,)152 153 def __reduce__(self):154 # Special pickle to zone remains a singleton and to cope with155 # database changes.156 return pytz._p, (self.zone,)157 158 159class DstTzInfo(BaseTzInfo):160 '''A timezone that has a variable offset from UTC161 162 The offset might change if daylight saving time comes into effect,163 or at a point in history when the region decides to change their164 timezone definition.165 '''166 # Overridden in subclass167 168 # Sorted list of DST transition times, UTC169 _utc_transition_times = None170 171 # [(utcoffset, dstoffset, tzname)] corresponding to172 # _utc_transition_times entries173 _transition_info = None174 175 zone = None176 177 # Set in __init__178 179 _tzinfos = None180 _dst = None # DST offset181 182 def __init__(self, _inf=None, _tzinfos=None):183 if _inf:184 self._tzinfos = _tzinfos185 self._utcoffset, self._dst, self._tzname = _inf186 else:187 _tzinfos = {}188 self._tzinfos = _tzinfos189 self._utcoffset, self._dst, self._tzname = (190 self._transition_info[0])191 _tzinfos[self._transition_info[0]] = self192 for inf in self._transition_info[1:]:193 if inf not in _tzinfos:194 _tzinfos[inf] = self.__class__(inf, _tzinfos)195 196 def fromutc(self, dt):197 '''See datetime.tzinfo.fromutc'''198 if (dt.tzinfo is not None and199 getattr(dt.tzinfo, '_tzinfos', None) is not self._tzinfos):200 raise ValueError('fromutc: dt.tzinfo is not self')201 dt = dt.replace(tzinfo=None)202 idx = max(0, bisect_right(self._utc_transition_times, dt) - 1)203 inf = self._transition_info[idx]204 return (dt + inf[0]).replace(tzinfo=self._tzinfos[inf])205 206 def normalize(self, dt):207 '''Correct the timezone information on the given datetime208 209 If date arithmetic crosses DST boundaries, the tzinfo210 is not magically adjusted. This method normalizes the211 tzinfo to the correct one.212 213 To test, first we need to do some setup214 215 >>> from pytz import timezone216 >>> utc = timezone('UTC')217 >>> eastern = timezone('US/Eastern')218 >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)'219 220 We next create a datetime right on an end-of-DST transition point,221 the instant when the wallclocks are wound back one hour.222 223 >>> utc_dt = datetime(2002, 10, 27, 6, 0, 0, tzinfo=utc)224 >>> loc_dt = utc_dt.astimezone(eastern)225 >>> loc_dt.strftime(fmt)226 '2002-10-27 01:00:00 EST (-0500)'227 228 Now, if we subtract a few minutes from it, note that the timezone229 information has not changed.230 231 >>> before = loc_dt - timedelta(minutes=10)232 >>> before.strftime(fmt)233 '2002-10-27 00:50:00 EST (-0500)'234 235 But we can fix that by calling the normalize method236 237 >>> before = eastern.normalize(before)238 >>> before.strftime(fmt)239 '2002-10-27 01:50:00 EDT (-0400)'240 241 The supported method of converting between timezones is to use242 datetime.astimezone(). Currently, normalize() also works:243 244 >>> th = timezone('Asia/Bangkok')245 >>> am = timezone('Europe/Amsterdam')246 >>> dt = th.localize(datetime(2011, 5, 7, 1, 2, 3))247 >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)'248 >>> am.normalize(dt).strftime(fmt)249 '2011-05-06 20:02:03 CEST (+0200)'250 '''251 if dt.tzinfo is None:252 raise ValueError('Naive time - no tzinfo set')253 254 # Convert dt in localtime to UTC255 offset = dt.tzinfo._utcoffset256 dt = dt.replace(tzinfo=None)257 dt = dt - offset258 # convert it back, and return it259 return self.fromutc(dt)260 261 def localize(self, dt, is_dst=False):262 '''Convert naive time to local time.263 264 This method should be used to construct localtimes, rather265 than passing a tzinfo argument to a datetime constructor.266 267 is_dst is used to determine the correct timezone in the ambigous268 period at the end of daylight saving time.269 270 >>> from pytz import timezone271 >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)'272 >>> amdam = timezone('Europe/Amsterdam')273 >>> dt = datetime(2004, 10, 31, 2, 0, 0)274 >>> loc_dt1 = amdam.localize(dt, is_dst=True)275 >>> loc_dt2 = amdam.localize(dt, is_dst=False)276 >>> loc_dt1.strftime(fmt)277 '2004-10-31 02:00:00 CEST (+0200)'278 >>> loc_dt2.strftime(fmt)279 '2004-10-31 02:00:00 CET (+0100)'280 >>> str(loc_dt2 - loc_dt1)281 '1:00:00'282 283 Use is_dst=None to raise an AmbiguousTimeError for ambiguous284 times at the end of daylight saving time285 286 >>> try:287 ... loc_dt1 = amdam.localize(dt, is_dst=None)288 ... except AmbiguousTimeError:289 ... print('Ambiguous')290 Ambiguous291 292 is_dst defaults to False293 294 >>> amdam.localize(dt) == amdam.localize(dt, False)295 True296 297 is_dst is also used to determine the correct timezone in the298 wallclock times jumped over at the start of daylight saving time.299 300 >>> pacific = timezone('US/Pacific')301 >>> dt = datetime(2008, 3, 9, 2, 0, 0)302 >>> ploc_dt1 = pacific.localize(dt, is_dst=True)303 >>> ploc_dt2 = pacific.localize(dt, is_dst=False)304 >>> ploc_dt1.strftime(fmt)305 '2008-03-09 02:00:00 PDT (-0700)'306 >>> ploc_dt2.strftime(fmt)307 '2008-03-09 02:00:00 PST (-0800)'308 >>> str(ploc_dt2 - ploc_dt1)309 '1:00:00'310 311 Use is_dst=None to raise a NonExistentTimeError for these skipped312 times.313 314 >>> try:315 ... loc_dt1 = pacific.localize(dt, is_dst=None)316 ... except NonExistentTimeError:317 ... print('Non-existent')318 Non-existent319 '''320 if dt.tzinfo is not None:321 raise ValueError('Not naive datetime (tzinfo is already set)')322 323 # Find the two best possibilities.324 possible_loc_dt = set()325 for delta in [timedelta(days=-1), timedelta(days=1)]:326 loc_dt = dt + delta327 idx = max(0, bisect_right(328 self._utc_transition_times, loc_dt) - 1)329 inf = self._transition_info[idx]330 tzinfo = self._tzinfos[inf]331 loc_dt = tzinfo.normalize(dt.replace(tzinfo=tzinfo))332 if loc_dt.replace(tzinfo=None) == dt:333 possible_loc_dt.add(loc_dt)334 335 if len(possible_loc_dt) == 1:336 return possible_loc_dt.pop()337 338 # If there are no possibly correct timezones, we are attempting339 # to convert a time that never happened - the time period jumped340 # during the start-of-DST transition period.341 if len(possible_loc_dt) == 0:342 # If we refuse to guess, raise an exception.343 if is_dst is None:344 raise NonExistentTimeError(dt)345 346 # If we are forcing the pre-DST side of the DST transition, we347 # obtain the correct timezone by winding the clock forward a few348 # hours.349 elif is_dst:350 return self.localize(351 dt + timedelta(hours=6), is_dst=True) - timedelta(hours=6)352 353 # If we are forcing the post-DST side of the DST transition, we354 # obtain the correct timezone by winding the clock back.355 else:356 return self.localize(357 dt - timedelta(hours=6),358 is_dst=False) + timedelta(hours=6)359 360 # If we get this far, we have multiple possible timezones - this361 # is an ambiguous case occurring during the end-of-DST transition.362 363 # If told to be strict, raise an exception since we have an364 # ambiguous case365 if is_dst is None:366 raise AmbiguousTimeError(dt)367 368 # Filter out the possiblilities that don't match the requested369 # is_dst370 filtered_possible_loc_dt = [371 p for p in possible_loc_dt if bool(p.tzinfo._dst) == is_dst372 ]373 374 # Hopefully we only have one possibility left. Return it.375 if len(filtered_possible_loc_dt) == 1:376 return filtered_possible_loc_dt[0]377 378 if len(filtered_possible_loc_dt) == 0:379 filtered_possible_loc_dt = list(possible_loc_dt)380 381 # If we get this far, we have in a wierd timezone transition382 # where the clocks have been wound back but is_dst is the same383 # in both (eg. Europe/Warsaw 1915 when they switched to CET).384 # At this point, we just have to guess unless we allow more385 # hints to be passed in (such as the UTC offset or abbreviation),386 # but that is just getting silly.387 #388 # Choose the earliest (by UTC) applicable timezone if is_dst=True389 # Choose the latest (by UTC) applicable timezone if is_dst=False390 # i.e., behave like end-of-DST transition391 dates = {} # utc -> local392 for local_dt in filtered_possible_loc_dt:393 utc_time = (394 local_dt.replace(tzinfo=None) - local_dt.tzinfo._utcoffset)395 assert utc_time not in dates396 dates[utc_time] = local_dt397 return dates[[min, max][not is_dst](dates)]398 399 def utcoffset(self, dt, is_dst=None):400 '''See datetime.tzinfo.utcoffset401 402 The is_dst parameter may be used to remove ambiguity during DST403 transitions.404 405 >>> from pytz import timezone406 >>> tz = timezone('America/St_Johns')407 >>> ambiguous = datetime(2009, 10, 31, 23, 30)408 409 >>> str(tz.utcoffset(ambiguous, is_dst=False))410 '-1 day, 20:30:00'411 412 >>> str(tz.utcoffset(ambiguous, is_dst=True))413 '-1 day, 21:30:00'414 415 >>> try:416 ... tz.utcoffset(ambiguous)417 ... except AmbiguousTimeError:418 ... print('Ambiguous')419 Ambiguous420 421 '''422 if dt is None:423 return None424 elif dt.tzinfo is not self:425 dt = self.localize(dt, is_dst)426 return dt.tzinfo._utcoffset427 else:428 return self._utcoffset429 430 def dst(self, dt, is_dst=None):431 '''See datetime.tzinfo.dst432 433 The is_dst parameter may be used to remove ambiguity during DST434 transitions.435 436 >>> from pytz import timezone437 >>> tz = timezone('America/St_Johns')438 439 >>> normal = datetime(2009, 9, 1)440 441 >>> str(tz.dst(normal))442 '1:00:00'443 >>> str(tz.dst(normal, is_dst=False))444 '1:00:00'445 >>> str(tz.dst(normal, is_dst=True))446 '1:00:00'447 448 >>> ambiguous = datetime(2009, 10, 31, 23, 30)449 450 >>> str(tz.dst(ambiguous, is_dst=False))451 '0:00:00'452 >>> str(tz.dst(ambiguous, is_dst=True))453 '1:00:00'454 >>> try:455 ... tz.dst(ambiguous)456 ... except AmbiguousTimeError:457 ... print('Ambiguous')458 Ambiguous459 460 '''461 if dt is None:462 return None463 elif dt.tzinfo is not self:464 dt = self.localize(dt, is_dst)465 return dt.tzinfo._dst466 else:467 return self._dst468 469 def tzname(self, dt, is_dst=None):470 '''See datetime.tzinfo.tzname471 472 The is_dst parameter may be used to remove ambiguity during DST473 transitions.474 475 >>> from pytz import timezone476 >>> tz = timezone('America/St_Johns')477 478 >>> normal = datetime(2009, 9, 1)479 480 >>> tz.tzname(normal)481 'NDT'482 >>> tz.tzname(normal, is_dst=False)483 'NDT'484 >>> tz.tzname(normal, is_dst=True)485 'NDT'486 487 >>> ambiguous = datetime(2009, 10, 31, 23, 30)488 489 >>> tz.tzname(ambiguous, is_dst=False)490 'NST'491 >>> tz.tzname(ambiguous, is_dst=True)492 'NDT'493 >>> try:494 ... tz.tzname(ambiguous)495 ... except AmbiguousTimeError:496 ... print('Ambiguous')497 Ambiguous498 '''499 if dt is None:500 return self.zone501 elif dt.tzinfo is not self:502 dt = self.localize(dt, is_dst)503 return dt.tzinfo._tzname504 else:505 return self._tzname506 507 def __repr__(self):508 if self._dst:509 dst = 'DST'510 else:511 dst = 'STD'512 if self._utcoffset > _notime:513 return '<DstTzInfo %r %s+%s %s>' % (514 self.zone, self._tzname, self._utcoffset, dst515 )516 else:517 return '<DstTzInfo %r %s%s %s>' % (518 self.zone, self._tzname, self._utcoffset, dst519 )520 521 def __reduce__(self):522 # Special pickle to zone remains a singleton and to cope with523 # database changes.524 return pytz._p, (525 self.zone,526 _to_seconds(self._utcoffset),527 _to_seconds(self._dst),528 self._tzname529 )530 531 532def unpickler(zone, utcoffset=None, dstoffset=None, tzname=None):533 """Factory function for unpickling pytz tzinfo instances.534 535 This is shared for both StaticTzInfo and DstTzInfo instances, because536 database changes could cause a zones implementation to switch between537 these two base classes and we can't break pickles on a pytz version538 upgrade.539 """540 # Raises a KeyError if zone no longer exists, which should never happen541 # and would be a bug.542 tz = pytz.timezone(zone)543 544 # A StaticTzInfo - just return it545 if utcoffset is None:546 return tz547 548 # This pickle was created from a DstTzInfo. We need to549 # determine which of the list of tzinfo instances for this zone550 # to use in order to restore the state of any datetime instances using551 # it correctly.552 utcoffset = memorized_timedelta(utcoffset)553 dstoffset = memorized_timedelta(dstoffset)554 try:555 return tz._tzinfos[(utcoffset, dstoffset, tzname)]556 except KeyError:557 # The particular state requested in this timezone no longer exists.558 # This indicates a corrupt pickle, or the timezone database has been559 # corrected violently enough to make this particular560 # (utcoffset,dstoffset) no longer exist in the zone, or the561 # abbreviation has been changed.562 pass563 564 # See if we can find an entry differing only by tzname. Abbreviations565 # get changed from the initial guess by the database maintainers to566 # match reality when this information is discovered.567 for localized_tz in tz._tzinfos.values():568 if (localized_tz._utcoffset == utcoffset and569 localized_tz._dst == dstoffset):570 return localized_tz571 572 # This (utcoffset, dstoffset) information has been removed from the573 # zone. Add it back. This might occur when the database maintainers have574 # corrected incorrect information. datetime instances using this575 # incorrect information will continue to do so, exactly as they were576 # before being pickled. This is purely an overly paranoid safety net - I577 # doubt this will ever been needed in real life.578 inf = (utcoffset, dstoffset, tzname)579 tz._tzinfos[inf] = tz.__class__(inf, tz._tzinfos)580 return tz._tzinfos[inf]581 