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