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
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] = dt
41 return dt
42
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] = ttinfo
58 return ttinfo
59
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 * 60
67
68
69class BaseTzInfo(tzinfo):
70 # Overridden in subclass
71 _utcoffset = None
72 _tzname = None
73 zone = None
74
75 def __str__(self):
76 return self.zone
77
78
79class StaticTzInfo(BaseTzInfo):
80 '''A timezone that has a constant offset from UTC
81
82 These timezones are rare, as most locations have changed their
83 offset at some point in their history
84 '''
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.utcoffset
93
94 is_dst is ignored for StaticTzInfo, and exists only to
95 retain compatibility with DstTzInfo.
96 '''
97 return self._utcoffset
98
99 def dst(self, dt, is_dst=None):
100 '''See datetime.tzinfo.dst
101
102 is_dst is ignored for StaticTzInfo, and exists only to
103 retain compatibility with DstTzInfo.
104 '''
105 return _notime
106
107 def tzname(self, dt, is_dst=None):
108 '''See datetime.tzinfo.tzname
109
110 is_dst is ignored for StaticTzInfo, and exists only to
111 retain compatibility with DstTzInfo.
112 '''
113 return self._tzname
114
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 have
125 ambiguous cases to correct:
126
127 >>> from pytz import timezone
128 >>> gmt = timezone('GMT')
129 >>> isinstance(gmt, StaticTzInfo)
130 True
131 >>> dt = datetime(2011, 5, 8, 1, 2, 3, tzinfo=gmt)
132 >>> gmt.normalize(dt) is dt
133 True
134
135 The supported method of converting between timezones is to use
136 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 dt
146 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 with
155 # database changes.
156 return pytz._p, (self.zone,)
157
158
159class DstTzInfo(BaseTzInfo):
160 '''A timezone that has a variable offset from UTC
161
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 their
164 timezone definition.
165 '''
166 # Overridden in subclass
167
168 # Sorted list of DST transition times, UTC
169 _utc_transition_times = None
170
171 # [(utcoffset, dstoffset, tzname)] corresponding to
172 # _utc_transition_times entries
173 _transition_info = None
174
175 zone = None
176
177 # Set in __init__
178
179 _tzinfos = None
180 _dst = None # DST offset
181
182 def __init__(self, _inf=None, _tzinfos=None):
183 if _inf:
184 self._tzinfos = _tzinfos
185 self._utcoffset, self._dst, self._tzname = _inf
186 else:
187 _tzinfos = {}
188 self._tzinfos = _tzinfos
189 self._utcoffset, self._dst, self._tzname = (
190 self._transition_info[0])
191 _tzinfos[self._transition_info[0]] = self
192 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 and
199 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 datetime
208
209 If date arithmetic crosses DST boundaries, the tzinfo
210 is not magically adjusted. This method normalizes the
211 tzinfo to the correct one.
212
213 To test, first we need to do some setup
214
215 >>> from pytz import timezone
216 >>> 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 timezone
229 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 method
236
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 use
242 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 UTC
255 offset = dt.tzinfo._utcoffset
256 dt = dt.replace(tzinfo=None)
257 dt = dt - offset
258 # convert it back, and return it
259 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, rather
265 than passing a tzinfo argument to a datetime constructor.
266
267 is_dst is used to determine the correct timezone in the ambigous
268 period at the end of daylight saving time.
269
270 >>> from pytz import timezone
271 >>> 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 ambiguous
284 times at the end of daylight saving time
285
286 >>> try:
287 ... loc_dt1 = amdam.localize(dt, is_dst=None)
288 ... except AmbiguousTimeError:
289 ... print('Ambiguous')
290 Ambiguous
291
292 is_dst defaults to False
293
294 >>> amdam.localize(dt) == amdam.localize(dt, False)
295 True
296
297 is_dst is also used to determine the correct timezone in the
298 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 skipped
312 times.
313
314 >>> try:
315 ... loc_dt1 = pacific.localize(dt, is_dst=None)
316 ... except NonExistentTimeError:
317 ... print('Non-existent')
318 Non-existent
319 '''
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 try:
327 loc_dt = dt + delta
328 except OverflowError:
329 # dt is close to datetime.min or datetime.max; skip this
330 # direction rather than raising an OverflowError to the caller.
331 continue
332 idx = max(0, bisect_right(
333 self._utc_transition_times, loc_dt) - 1)
334 inf = self._transition_info[idx]
335 tzinfo = self._tzinfos[inf]
336 loc_dt = tzinfo.normalize(dt.replace(tzinfo=tzinfo))
337 if loc_dt.replace(tzinfo=None) == dt:
338 possible_loc_dt.add(loc_dt)
339
340 if len(possible_loc_dt) == 1:
341 return possible_loc_dt.pop()
342
343 # If there are no possibly correct timezones, we are attempting
344 # to convert a time that never happened - the time period jumped
345 # during the start-of-DST transition period.
346 if len(possible_loc_dt) == 0:
347 # If we refuse to guess, raise an exception.
348 if is_dst is None:
349 raise NonExistentTimeError(dt)
350
351 # If we are forcing the pre-DST side of the DST transition, we
352 # obtain the correct timezone by winding the clock forward a few
353 # hours.
354 elif is_dst:
355 return self.localize(
356 dt + timedelta(hours=6), is_dst=True) - timedelta(hours=6)
357
358 # If we are forcing the post-DST side of the DST transition, we
359 # obtain the correct timezone by winding the clock back.
360 else:
361 return self.localize(
362 dt - timedelta(hours=6),
363 is_dst=False) + timedelta(hours=6)
364
365 # If we get this far, we have multiple possible timezones - this
366 # is an ambiguous case occurring during the end-of-DST transition.
367
368 # If told to be strict, raise an exception since we have an
369 # ambiguous case
370 if is_dst is None:
371 raise AmbiguousTimeError(dt)
372
373 # Filter out the possiblilities that don't match the requested
374 # is_dst
375 filtered_possible_loc_dt = [
376 p for p in possible_loc_dt if bool(p.tzinfo._dst) == is_dst
377 ]
378
379 # Hopefully we only have one possibility left. Return it.
380 if len(filtered_possible_loc_dt) == 1:
381 return filtered_possible_loc_dt[0]
382
383 if len(filtered_possible_loc_dt) == 0:
384 filtered_possible_loc_dt = list(possible_loc_dt)
385
386 # If we get this far, we have in a wierd timezone transition
387 # where the clocks have been wound back but is_dst is the same
388 # in both (eg. Europe/Warsaw 1915 when they switched to CET).
389 # At this point, we just have to guess unless we allow more
390 # hints to be passed in (such as the UTC offset or abbreviation),
391 # but that is just getting silly.
392 #
393 # Choose the earliest (by UTC) applicable timezone if is_dst=True
394 # Choose the latest (by UTC) applicable timezone if is_dst=False
395 # i.e., behave like end-of-DST transition
396 dates = {} # utc -> local
397 for local_dt in filtered_possible_loc_dt:
398 utc_time = (
399 local_dt.replace(tzinfo=None) - local_dt.tzinfo._utcoffset)
400 assert utc_time not in dates
401 dates[utc_time] = local_dt
402 return dates[[min, max][not is_dst](dates)]
403
404 def utcoffset(self, dt, is_dst=None):
405 '''See datetime.tzinfo.utcoffset
406
407 The is_dst parameter may be used to remove ambiguity during DST
408 transitions.
409
410 >>> from pytz import timezone
411 >>> tz = timezone('America/St_Johns')
412 >>> ambiguous = datetime(2009, 10, 31, 23, 30)
413
414 >>> str(tz.utcoffset(ambiguous, is_dst=False))
415 '-1 day, 20:30:00'
416
417 >>> str(tz.utcoffset(ambiguous, is_dst=True))
418 '-1 day, 21:30:00'
419
420 >>> try:
421 ... tz.utcoffset(ambiguous)
422 ... except AmbiguousTimeError:
423 ... print('Ambiguous')
424 Ambiguous
425
426 '''
427 if dt is None:
428 return None
429 elif dt.tzinfo is not self:
430 dt = self.localize(dt, is_dst)
431 return dt.tzinfo._utcoffset
432 else:
433 return self._utcoffset
434
435 def dst(self, dt, is_dst=None):
436 '''See datetime.tzinfo.dst
437
438 The is_dst parameter may be used to remove ambiguity during DST
439 transitions.
440
441 >>> from pytz import timezone
442 >>> tz = timezone('America/St_Johns')
443
444 >>> normal = datetime(2009, 9, 1)
445
446 >>> str(tz.dst(normal))
447 '1:00:00'
448 >>> str(tz.dst(normal, is_dst=False))
449 '1:00:00'
450 >>> str(tz.dst(normal, is_dst=True))
451 '1:00:00'
452
453 >>> ambiguous = datetime(2009, 10, 31, 23, 30)
454
455 >>> str(tz.dst(ambiguous, is_dst=False))
456 '0:00:00'
457 >>> str(tz.dst(ambiguous, is_dst=True))
458 '1:00:00'
459 >>> try:
460 ... tz.dst(ambiguous)
461 ... except AmbiguousTimeError:
462 ... print('Ambiguous')
463 Ambiguous
464
465 '''
466 if dt is None:
467 return None
468 elif dt.tzinfo is not self:
469 dt = self.localize(dt, is_dst)
470 return dt.tzinfo._dst
471 else:
472 return self._dst
473
474 def tzname(self, dt, is_dst=None):
475 '''See datetime.tzinfo.tzname
476
477 The is_dst parameter may be used to remove ambiguity during DST
478 transitions.
479
480 >>> from pytz import timezone
481 >>> tz = timezone('America/St_Johns')
482
483 >>> normal = datetime(2009, 9, 1)
484
485 >>> tz.tzname(normal)
486 'NDT'
487 >>> tz.tzname(normal, is_dst=False)
488 'NDT'
489 >>> tz.tzname(normal, is_dst=True)
490 'NDT'
491
492 >>> ambiguous = datetime(2009, 10, 31, 23, 30)
493
494 >>> tz.tzname(ambiguous, is_dst=False)
495 'NST'
496 >>> tz.tzname(ambiguous, is_dst=True)
497 'NDT'
498 >>> try:
499 ... tz.tzname(ambiguous)
500 ... except AmbiguousTimeError:
501 ... print('Ambiguous')
502 Ambiguous
503 '''
504 if dt is None:
505 return self.zone
506 elif dt.tzinfo is not self:
507 dt = self.localize(dt, is_dst)
508 return dt.tzinfo._tzname
509 else:
510 return self._tzname
511
512 def __repr__(self):
513 if self._dst:
514 dst = 'DST'
515 else:
516 dst = 'STD'
517 if self._utcoffset > _notime:
518 return '<DstTzInfo %r %s+%s %s>' % (
519 self.zone, self._tzname, self._utcoffset, dst
520 )
521 else:
522 return '<DstTzInfo %r %s%s %s>' % (
523 self.zone, self._tzname, self._utcoffset, dst
524 )
525
526 def __reduce__(self):
527 # Special pickle to zone remains a singleton and to cope with
528 # database changes.
529 return pytz._p, (
530 self.zone,
531 _to_seconds(self._utcoffset),
532 _to_seconds(self._dst),
533 self._tzname
534 )
535
536
537def unpickler(zone, utcoffset=None, dstoffset=None, tzname=None):
538 """Factory function for unpickling pytz tzinfo instances.
539
540 This is shared for both StaticTzInfo and DstTzInfo instances, because
541 database changes could cause a zones implementation to switch between
542 these two base classes and we can't break pickles on a pytz version
543 upgrade.
544 """
545 # Raises a KeyError if zone no longer exists, which should never happen
546 # and would be a bug.
547 tz = pytz.timezone(zone)
548
549 # A StaticTzInfo - just return it
550 if utcoffset is None:
551 return tz
552
553 # This pickle was created from a DstTzInfo. We need to
554 # determine which of the list of tzinfo instances for this zone
555 # to use in order to restore the state of any datetime instances using
556 # it correctly.
557 utcoffset = memorized_timedelta(utcoffset)
558 dstoffset = memorized_timedelta(dstoffset)
559 try:
560 return tz._tzinfos[(utcoffset, dstoffset, tzname)]
561 except KeyError:
562 # The particular state requested in this timezone no longer exists.
563 # This indicates a corrupt pickle, or the timezone database has been
564 # corrected violently enough to make this particular
565 # (utcoffset,dstoffset) no longer exist in the zone, or the
566 # abbreviation has been changed.
567 pass
568
569 # See if we can find an entry differing only by tzname. Abbreviations
570 # get changed from the initial guess by the database maintainers to
571 # match reality when this information is discovered.
572 for localized_tz in tz._tzinfos.values():
573 if (localized_tz._utcoffset == utcoffset and
574 localized_tz._dst == dstoffset):
575 return localized_tz
576
577 # This (utcoffset, dstoffset) information has been removed from the
578 # zone. Add it back. This might occur when the database maintainers have
579 # corrected incorrect information. datetime instances using this
580 # incorrect information will continue to do so, exactly as they were
581 # before being pickled. This is purely an overly paranoid safety net - I
582 # doubt this will ever been needed in real life.
583 inf = (utcoffset, dstoffset, tzname)
584 tz._tzinfos[inf] = tz.__class__(inf, tz._tzinfos)
585 return tz._tzinfos[inf]