Mercurial > repos > rliterman > csp2
comparison CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/python3.8/site-packages/pytz/tzinfo.py @ 68:5028fdace37b
planemo upload commit 2e9511a184a1ca667c7be0c6321a36dc4e3d116d
author | jpayne |
---|---|
date | Tue, 18 Mar 2025 16:23:26 -0400 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
67:0e9998148a16 | 68:5028fdace37b |
---|---|
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] |