Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/pytz/tzinfo.py: 29%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1'''Base classes and helpers for building zone specific tzinfo classes'''
3from datetime import datetime, timedelta, tzinfo
4from bisect import bisect_right
5try:
6 set
7except NameError:
8 from sets import Set as set
10import pytz
11from pytz.exceptions import AmbiguousTimeError, NonExistentTimeError
13__all__ = []
15_timedelta_cache = {}
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
28_epoch = datetime(1970, 1, 1, 0, 0) # datetime.utcfromtimestamp(0)
29_datetime_cache = {0: _epoch}
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
44_ttinfo_cache = {}
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
61_notime = memorized_timedelta(0)
64def _to_seconds(td):
65 '''Convert a timedelta to seconds'''
66 return td.seconds + td.days * 24 * 60 * 60
69class BaseTzInfo(tzinfo):
70 # Overridden in subclass
71 _utcoffset = None
72 _tzname = None
73 zone = None
75 def __str__(self):
76 return self.zone
79class StaticTzInfo(BaseTzInfo):
80 '''A timezone that has a constant offset from UTC
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)
91 def utcoffset(self, dt, is_dst=None):
92 '''See datetime.tzinfo.utcoffset
94 is_dst is ignored for StaticTzInfo, and exists only to
95 retain compatibility with DstTzInfo.
96 '''
97 return self._utcoffset
99 def dst(self, dt, is_dst=None):
100 '''See datetime.tzinfo.dst
102 is_dst is ignored for StaticTzInfo, and exists only to
103 retain compatibility with DstTzInfo.
104 '''
105 return _notime
107 def tzname(self, dt, is_dst=None):
108 '''See datetime.tzinfo.tzname
110 is_dst is ignored for StaticTzInfo, and exists only to
111 retain compatibility with DstTzInfo.
112 '''
113 return self._tzname
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)
121 def normalize(self, dt, is_dst=False):
122 '''Correct the timezone information on the given datetime.
124 This is normally a no-op, as StaticTzInfo timezones never have
125 ambiguous cases to correct:
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
135 The supported method of converting between timezones is to use
136 datetime.astimezone(). Currently normalize() also works:
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)
150 def __repr__(self):
151 return '<StaticTzInfo %r>' % (self.zone,)
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,)
159class DstTzInfo(BaseTzInfo):
160 '''A timezone that has a variable offset from UTC
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
168 # Sorted list of DST transition times, UTC
169 _utc_transition_times = None
171 # [(utcoffset, dstoffset, tzname)] corresponding to
172 # _utc_transition_times entries
173 _transition_info = None
175 zone = None
177 # Set in __init__
179 _tzinfos = None
180 _dst = None # DST offset
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)
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])
206 def normalize(self, dt):
207 '''Correct the timezone information on the given datetime
209 If date arithmetic crosses DST boundaries, the tzinfo
210 is not magically adjusted. This method normalizes the
211 tzinfo to the correct one.
213 To test, first we need to do some setup
215 >>> from pytz import timezone
216 >>> utc = timezone('UTC')
217 >>> eastern = timezone('US/Eastern')
218 >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)'
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.
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)'
228 Now, if we subtract a few minutes from it, note that the timezone
229 information has not changed.
231 >>> before = loc_dt - timedelta(minutes=10)
232 >>> before.strftime(fmt)
233 '2002-10-27 00:50:00 EST (-0500)'
235 But we can fix that by calling the normalize method
237 >>> before = eastern.normalize(before)
238 >>> before.strftime(fmt)
239 '2002-10-27 01:50:00 EDT (-0400)'
241 The supported method of converting between timezones is to use
242 datetime.astimezone(). Currently, normalize() also works:
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')
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)
261 def localize(self, dt, is_dst=False):
262 '''Convert naive time to local time.
264 This method should be used to construct localtimes, rather
265 than passing a tzinfo argument to a datetime constructor.
267 is_dst is used to determine the correct timezone in the ambigous
268 period at the end of daylight saving time.
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'
283 Use is_dst=None to raise an AmbiguousTimeError for ambiguous
284 times at the end of daylight saving time
286 >>> try:
287 ... loc_dt1 = amdam.localize(dt, is_dst=None)
288 ... except AmbiguousTimeError:
289 ... print('Ambiguous')
290 Ambiguous
292 is_dst defaults to False
294 >>> amdam.localize(dt) == amdam.localize(dt, False)
295 True
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.
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'
311 Use is_dst=None to raise a NonExistentTimeError for these skipped
312 times.
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)')
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 + delta
327 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)
335 if len(possible_loc_dt) == 1:
336 return possible_loc_dt.pop()
338 # If there are no possibly correct timezones, we are attempting
339 # to convert a time that never happened - the time period jumped
340 # 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)
346 # If we are forcing the pre-DST side of the DST transition, we
347 # obtain the correct timezone by winding the clock forward a few
348 # hours.
349 elif is_dst:
350 return self.localize(
351 dt + timedelta(hours=6), is_dst=True) - timedelta(hours=6)
353 # If we are forcing the post-DST side of the DST transition, we
354 # 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)
360 # If we get this far, we have multiple possible timezones - this
361 # is an ambiguous case occurring during the end-of-DST transition.
363 # If told to be strict, raise an exception since we have an
364 # ambiguous case
365 if is_dst is None:
366 raise AmbiguousTimeError(dt)
368 # Filter out the possiblilities that don't match the requested
369 # is_dst
370 filtered_possible_loc_dt = [
371 p for p in possible_loc_dt if bool(p.tzinfo._dst) == is_dst
372 ]
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]
378 if len(filtered_possible_loc_dt) == 0:
379 filtered_possible_loc_dt = list(possible_loc_dt)
381 # If we get this far, we have in a wierd timezone transition
382 # where the clocks have been wound back but is_dst is the same
383 # in both (eg. Europe/Warsaw 1915 when they switched to CET).
384 # At this point, we just have to guess unless we allow more
385 # 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=True
389 # Choose the latest (by UTC) applicable timezone if is_dst=False
390 # i.e., behave like end-of-DST transition
391 dates = {} # utc -> local
392 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 dates
396 dates[utc_time] = local_dt
397 return dates[[min, max][not is_dst](dates)]
399 def utcoffset(self, dt, is_dst=None):
400 '''See datetime.tzinfo.utcoffset
402 The is_dst parameter may be used to remove ambiguity during DST
403 transitions.
405 >>> from pytz import timezone
406 >>> tz = timezone('America/St_Johns')
407 >>> ambiguous = datetime(2009, 10, 31, 23, 30)
409 >>> str(tz.utcoffset(ambiguous, is_dst=False))
410 '-1 day, 20:30:00'
412 >>> str(tz.utcoffset(ambiguous, is_dst=True))
413 '-1 day, 21:30:00'
415 >>> try:
416 ... tz.utcoffset(ambiguous)
417 ... except AmbiguousTimeError:
418 ... print('Ambiguous')
419 Ambiguous
421 '''
422 if dt is None:
423 return None
424 elif dt.tzinfo is not self:
425 dt = self.localize(dt, is_dst)
426 return dt.tzinfo._utcoffset
427 else:
428 return self._utcoffset
430 def dst(self, dt, is_dst=None):
431 '''See datetime.tzinfo.dst
433 The is_dst parameter may be used to remove ambiguity during DST
434 transitions.
436 >>> from pytz import timezone
437 >>> tz = timezone('America/St_Johns')
439 >>> normal = datetime(2009, 9, 1)
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'
448 >>> ambiguous = datetime(2009, 10, 31, 23, 30)
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 Ambiguous
460 '''
461 if dt is None:
462 return None
463 elif dt.tzinfo is not self:
464 dt = self.localize(dt, is_dst)
465 return dt.tzinfo._dst
466 else:
467 return self._dst
469 def tzname(self, dt, is_dst=None):
470 '''See datetime.tzinfo.tzname
472 The is_dst parameter may be used to remove ambiguity during DST
473 transitions.
475 >>> from pytz import timezone
476 >>> tz = timezone('America/St_Johns')
478 >>> normal = datetime(2009, 9, 1)
480 >>> tz.tzname(normal)
481 'NDT'
482 >>> tz.tzname(normal, is_dst=False)
483 'NDT'
484 >>> tz.tzname(normal, is_dst=True)
485 'NDT'
487 >>> ambiguous = datetime(2009, 10, 31, 23, 30)
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 Ambiguous
498 '''
499 if dt is None:
500 return self.zone
501 elif dt.tzinfo is not self:
502 dt = self.localize(dt, is_dst)
503 return dt.tzinfo._tzname
504 else:
505 return self._tzname
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, dst
515 )
516 else:
517 return '<DstTzInfo %r %s%s %s>' % (
518 self.zone, self._tzname, self._utcoffset, dst
519 )
521 def __reduce__(self):
522 # Special pickle to zone remains a singleton and to cope with
523 # database changes.
524 return pytz._p, (
525 self.zone,
526 _to_seconds(self._utcoffset),
527 _to_seconds(self._dst),
528 self._tzname
529 )
532def unpickler(zone, utcoffset=None, dstoffset=None, tzname=None):
533 """Factory function for unpickling pytz tzinfo instances.
535 This is shared for both StaticTzInfo and DstTzInfo instances, because
536 database changes could cause a zones implementation to switch between
537 these two base classes and we can't break pickles on a pytz version
538 upgrade.
539 """
540 # Raises a KeyError if zone no longer exists, which should never happen
541 # and would be a bug.
542 tz = pytz.timezone(zone)
544 # A StaticTzInfo - just return it
545 if utcoffset is None:
546 return tz
548 # This pickle was created from a DstTzInfo. We need to
549 # determine which of the list of tzinfo instances for this zone
550 # to use in order to restore the state of any datetime instances using
551 # 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 been
559 # corrected violently enough to make this particular
560 # (utcoffset,dstoffset) no longer exist in the zone, or the
561 # abbreviation has been changed.
562 pass
564 # See if we can find an entry differing only by tzname. Abbreviations
565 # get changed from the initial guess by the database maintainers to
566 # match reality when this information is discovered.
567 for localized_tz in tz._tzinfos.values():
568 if (localized_tz._utcoffset == utcoffset and
569 localized_tz._dst == dstoffset):
570 return localized_tz
572 # This (utcoffset, dstoffset) information has been removed from the
573 # zone. Add it back. This might occur when the database maintainers have
574 # corrected incorrect information. datetime instances using this
575 # incorrect information will continue to do so, exactly as they were
576 # before being pickled. This is purely an overly paranoid safety net - I
577 # 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]