comparison CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/python3.8/site-packages/pytz/tzinfo.py @ 69:33d812a61356

planemo upload commit 2e9511a184a1ca667c7be0c6321a36dc4e3d116d
author jpayne
date Tue, 18 Mar 2025 17:55:14 -0400
parents
children
comparison
equal deleted inserted replaced
67:0e9998148a16 69:33d812a61356
1 '''Base classes and helpers for building zone specific tzinfo classes'''
2
3 from datetime import datetime, timedelta, tzinfo
4 from bisect import bisect_right
5 try:
6 set
7 except NameError:
8 from sets import Set as set
9
10 import pytz
11 from pytz.exceptions import AmbiguousTimeError, NonExistentTimeError
12
13 __all__ = []
14
15 _timedelta_cache = {}
16
17
18 def 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
32 def 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
47 def 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
64 def _to_seconds(td):
65 '''Convert a timedelta to seconds'''
66 return td.seconds + td.days * 24 * 60 * 60
67
68
69 class 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
79 class 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
159 class 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 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)
334
335 if len(possible_loc_dt) == 1:
336 return possible_loc_dt.pop()
337
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)
345
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)
352
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)
359
360 # If we get this far, we have multiple possible timezones - this
361 # is an ambiguous case occurring during the end-of-DST transition.
362
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)
367
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 ]
373
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]
377
378 if len(filtered_possible_loc_dt) == 0:
379 filtered_possible_loc_dt = list(possible_loc_dt)
380
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)]
398
399 def utcoffset(self, dt, is_dst=None):
400 '''See datetime.tzinfo.utcoffset
401
402 The is_dst parameter may be used to remove ambiguity during DST
403 transitions.
404
405 >>> from pytz import timezone
406 >>> tz = timezone('America/St_Johns')
407 >>> ambiguous = datetime(2009, 10, 31, 23, 30)
408
409 >>> str(tz.utcoffset(ambiguous, is_dst=False))
410 '-1 day, 20:30:00'
411
412 >>> str(tz.utcoffset(ambiguous, is_dst=True))
413 '-1 day, 21:30:00'
414
415 >>> try:
416 ... tz.utcoffset(ambiguous)
417 ... except AmbiguousTimeError:
418 ... print('Ambiguous')
419 Ambiguous
420
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
429
430 def dst(self, dt, is_dst=None):
431 '''See datetime.tzinfo.dst
432
433 The is_dst parameter may be used to remove ambiguity during DST
434 transitions.
435
436 >>> from pytz import timezone
437 >>> tz = timezone('America/St_Johns')
438
439 >>> normal = datetime(2009, 9, 1)
440
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'
447
448 >>> ambiguous = datetime(2009, 10, 31, 23, 30)
449
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
459
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
468
469 def tzname(self, dt, is_dst=None):
470 '''See datetime.tzinfo.tzname
471
472 The is_dst parameter may be used to remove ambiguity during DST
473 transitions.
474
475 >>> from pytz import timezone
476 >>> tz = timezone('America/St_Johns')
477
478 >>> normal = datetime(2009, 9, 1)
479
480 >>> tz.tzname(normal)
481 'NDT'
482 >>> tz.tzname(normal, is_dst=False)
483 'NDT'
484 >>> tz.tzname(normal, is_dst=True)
485 'NDT'
486
487 >>> ambiguous = datetime(2009, 10, 31, 23, 30)
488
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
506
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 )
520
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 )
530
531
532 def unpickler(zone, utcoffset=None, dstoffset=None, tzname=None):
533 """Factory function for unpickling pytz tzinfo instances.
534
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)
543
544 # A StaticTzInfo - just return it
545 if utcoffset is None:
546 return tz
547
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
563
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
571
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]