Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/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 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)
340 if len(possible_loc_dt) == 1:
341 return possible_loc_dt.pop()
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)
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)
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)
365 # If we get this far, we have multiple possible timezones - this
366 # is an ambiguous case occurring during the end-of-DST transition.
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)
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 ]
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]
383 if len(filtered_possible_loc_dt) == 0:
384 filtered_possible_loc_dt = list(possible_loc_dt)
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)]
404 def utcoffset(self, dt, is_dst=None):
405 '''See datetime.tzinfo.utcoffset
407 The is_dst parameter may be used to remove ambiguity during DST
408 transitions.
410 >>> from pytz import timezone
411 >>> tz = timezone('America/St_Johns')
412 >>> ambiguous = datetime(2009, 10, 31, 23, 30)
414 >>> str(tz.utcoffset(ambiguous, is_dst=False))
415 '-1 day, 20:30:00'
417 >>> str(tz.utcoffset(ambiguous, is_dst=True))
418 '-1 day, 21:30:00'
420 >>> try:
421 ... tz.utcoffset(ambiguous)
422 ... except AmbiguousTimeError:
423 ... print('Ambiguous')
424 Ambiguous
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
435 def dst(self, dt, is_dst=None):
436 '''See datetime.tzinfo.dst
438 The is_dst parameter may be used to remove ambiguity during DST
439 transitions.
441 >>> from pytz import timezone
442 >>> tz = timezone('America/St_Johns')
444 >>> normal = datetime(2009, 9, 1)
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'
453 >>> ambiguous = datetime(2009, 10, 31, 23, 30)
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
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
474 def tzname(self, dt, is_dst=None):
475 '''See datetime.tzinfo.tzname
477 The is_dst parameter may be used to remove ambiguity during DST
478 transitions.
480 >>> from pytz import timezone
481 >>> tz = timezone('America/St_Johns')
483 >>> normal = datetime(2009, 9, 1)
485 >>> tz.tzname(normal)
486 'NDT'
487 >>> tz.tzname(normal, is_dst=False)
488 'NDT'
489 >>> tz.tzname(normal, is_dst=True)
490 'NDT'
492 >>> ambiguous = datetime(2009, 10, 31, 23, 30)
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
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 )
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 )
537def unpickler(zone, utcoffset=None, dstoffset=None, tzname=None):
538 """Factory function for unpickling pytz tzinfo instances.
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)
549 # A StaticTzInfo - just return it
550 if utcoffset is None:
551 return tz
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
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
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]