Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/pytz/tzinfo.py: 23%
176 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
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
27_epoch = datetime.utcfromtimestamp(0)
28_datetime_cache = {0: _epoch}
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
42_ttinfo_cache = {}
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
58_notime = memorized_timedelta(0)
61def _to_seconds(td):
62 '''Convert a timedelta to seconds'''
63 return td.seconds + td.days * 24 * 60 * 60
66class BaseTzInfo(tzinfo):
67 # Overridden in subclass
68 _utcoffset = None
69 _tzname = None
70 zone = None
72 def __str__(self):
73 return self.zone
76class StaticTzInfo(BaseTzInfo):
77 '''A timezone that has a constant offset from UTC
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)
88 def utcoffset(self, dt, is_dst=None):
89 '''See datetime.tzinfo.utcoffset
91 is_dst is ignored for StaticTzInfo, and exists only to
92 retain compatibility with DstTzInfo.
93 '''
94 return self._utcoffset
96 def dst(self, dt, is_dst=None):
97 '''See datetime.tzinfo.dst
99 is_dst is ignored for StaticTzInfo, and exists only to
100 retain compatibility with DstTzInfo.
101 '''
102 return _notime
104 def tzname(self, dt, is_dst=None):
105 '''See datetime.tzinfo.tzname
107 is_dst is ignored for StaticTzInfo, and exists only to
108 retain compatibility with DstTzInfo.
109 '''
110 return self._tzname
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)
118 def normalize(self, dt, is_dst=False):
119 '''Correct the timezone information on the given datetime.
121 This is normally a no-op, as StaticTzInfo timezones never have
122 ambiguous cases to correct:
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
132 The supported method of converting between timezones is to use
133 datetime.astimezone(). Currently normalize() also works:
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)
147 def __repr__(self):
148 return '<StaticTzInfo %r>' % (self.zone,)
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,)
156class DstTzInfo(BaseTzInfo):
157 '''A timezone that has a variable offset from UTC
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
165 # Sorted list of DST transition times, UTC
166 _utc_transition_times = None
168 # [(utcoffset, dstoffset, tzname)] corresponding to
169 # _utc_transition_times entries
170 _transition_info = None
172 zone = None
174 # Set in __init__
176 _tzinfos = None
177 _dst = None # DST offset
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)
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])
203 def normalize(self, dt):
204 '''Correct the timezone information on the given datetime
206 If date arithmetic crosses DST boundaries, the tzinfo
207 is not magically adjusted. This method normalizes the
208 tzinfo to the correct one.
210 To test, first we need to do some setup
212 >>> from pytz import timezone
213 >>> utc = timezone('UTC')
214 >>> eastern = timezone('US/Eastern')
215 >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)'
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.
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)'
225 Now, if we subtract a few minutes from it, note that the timezone
226 information has not changed.
228 >>> before = loc_dt - timedelta(minutes=10)
229 >>> before.strftime(fmt)
230 '2002-10-27 00:50:00 EST (-0500)'
232 But we can fix that by calling the normalize method
234 >>> before = eastern.normalize(before)
235 >>> before.strftime(fmt)
236 '2002-10-27 01:50:00 EDT (-0400)'
238 The supported method of converting between timezones is to use
239 datetime.astimezone(). Currently, normalize() also works:
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')
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)
258 def localize(self, dt, is_dst=False):
259 '''Convert naive time to local time.
261 This method should be used to construct localtimes, rather
262 than passing a tzinfo argument to a datetime constructor.
264 is_dst is used to determine the correct timezone in the ambigous
265 period at the end of daylight saving time.
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'
280 Use is_dst=None to raise an AmbiguousTimeError for ambiguous
281 times at the end of daylight saving time
283 >>> try:
284 ... loc_dt1 = amdam.localize(dt, is_dst=None)
285 ... except AmbiguousTimeError:
286 ... print('Ambiguous')
287 Ambiguous
289 is_dst defaults to False
291 >>> amdam.localize(dt) == amdam.localize(dt, False)
292 True
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.
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'
308 Use is_dst=None to raise a NonExistentTimeError for these skipped
309 times.
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)')
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)
332 if len(possible_loc_dt) == 1:
333 return possible_loc_dt.pop()
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)
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)
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)
357 # If we get this far, we have multiple possible timezones - this
358 # is an ambiguous case occuring during the end-of-DST transition.
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)
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 ]
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]
375 if len(filtered_possible_loc_dt) == 0:
376 filtered_possible_loc_dt = list(possible_loc_dt)
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)]
396 def utcoffset(self, dt, is_dst=None):
397 '''See datetime.tzinfo.utcoffset
399 The is_dst parameter may be used to remove ambiguity during DST
400 transitions.
402 >>> from pytz import timezone
403 >>> tz = timezone('America/St_Johns')
404 >>> ambiguous = datetime(2009, 10, 31, 23, 30)
406 >>> str(tz.utcoffset(ambiguous, is_dst=False))
407 '-1 day, 20:30:00'
409 >>> str(tz.utcoffset(ambiguous, is_dst=True))
410 '-1 day, 21:30:00'
412 >>> try:
413 ... tz.utcoffset(ambiguous)
414 ... except AmbiguousTimeError:
415 ... print('Ambiguous')
416 Ambiguous
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
427 def dst(self, dt, is_dst=None):
428 '''See datetime.tzinfo.dst
430 The is_dst parameter may be used to remove ambiguity during DST
431 transitions.
433 >>> from pytz import timezone
434 >>> tz = timezone('America/St_Johns')
436 >>> normal = datetime(2009, 9, 1)
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'
445 >>> ambiguous = datetime(2009, 10, 31, 23, 30)
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
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
466 def tzname(self, dt, is_dst=None):
467 '''See datetime.tzinfo.tzname
469 The is_dst parameter may be used to remove ambiguity during DST
470 transitions.
472 >>> from pytz import timezone
473 >>> tz = timezone('America/St_Johns')
475 >>> normal = datetime(2009, 9, 1)
477 >>> tz.tzname(normal)
478 'NDT'
479 >>> tz.tzname(normal, is_dst=False)
480 'NDT'
481 >>> tz.tzname(normal, is_dst=True)
482 'NDT'
484 >>> ambiguous = datetime(2009, 10, 31, 23, 30)
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
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 )
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 )
529def unpickler(zone, utcoffset=None, dstoffset=None, tzname=None):
530 """Factory function for unpickling pytz tzinfo instances.
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)
541 # A StaticTzInfo - just return it
542 if utcoffset is None:
543 return tz
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
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
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]