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

182 statements  

1'''Base classes and helpers for building zone specific tzinfo classes''' 

2 

3from datetime import datetime, timedelta, tzinfo 

4from bisect import bisect_right 

5try: 

6 set 

7except NameError: 

8 from sets import Set as set 

9 

10import pytz 

11from pytz.exceptions import AmbiguousTimeError, NonExistentTimeError 

12 

13__all__ = [] 

14 

15_timedelta_cache = {} 

16 

17 

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 

26 

27 

28_epoch = datetime(1970, 1, 1, 0, 0) # datetime.utcfromtimestamp(0) 

29_datetime_cache = {0: _epoch} 

30 

31 

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 

42 

43 

44_ttinfo_cache = {} 

45 

46 

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 

59 

60 

61_notime = memorized_timedelta(0) 

62 

63 

64def _to_seconds(td): 

65 '''Convert a timedelta to seconds''' 

66 return td.seconds + td.days * 24 * 60 * 60 

67 

68 

69class 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 

79class 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 

159class 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 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) 

339 

340 if len(possible_loc_dt) == 1: 

341 return possible_loc_dt.pop() 

342 

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) 

350 

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) 

357 

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) 

364 

365 # If we get this far, we have multiple possible timezones - this 

366 # is an ambiguous case occurring during the end-of-DST transition. 

367 

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) 

372 

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 ] 

378 

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] 

382 

383 if len(filtered_possible_loc_dt) == 0: 

384 filtered_possible_loc_dt = list(possible_loc_dt) 

385 

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)] 

403 

404 def utcoffset(self, dt, is_dst=None): 

405 '''See datetime.tzinfo.utcoffset 

406 

407 The is_dst parameter may be used to remove ambiguity during DST 

408 transitions. 

409 

410 >>> from pytz import timezone 

411 >>> tz = timezone('America/St_Johns') 

412 >>> ambiguous = datetime(2009, 10, 31, 23, 30) 

413 

414 >>> str(tz.utcoffset(ambiguous, is_dst=False)) 

415 '-1 day, 20:30:00' 

416 

417 >>> str(tz.utcoffset(ambiguous, is_dst=True)) 

418 '-1 day, 21:30:00' 

419 

420 >>> try: 

421 ... tz.utcoffset(ambiguous) 

422 ... except AmbiguousTimeError: 

423 ... print('Ambiguous') 

424 Ambiguous 

425 

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 

434 

435 def dst(self, dt, is_dst=None): 

436 '''See datetime.tzinfo.dst 

437 

438 The is_dst parameter may be used to remove ambiguity during DST 

439 transitions. 

440 

441 >>> from pytz import timezone 

442 >>> tz = timezone('America/St_Johns') 

443 

444 >>> normal = datetime(2009, 9, 1) 

445 

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' 

452 

453 >>> ambiguous = datetime(2009, 10, 31, 23, 30) 

454 

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 

464 

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 

473 

474 def tzname(self, dt, is_dst=None): 

475 '''See datetime.tzinfo.tzname 

476 

477 The is_dst parameter may be used to remove ambiguity during DST 

478 transitions. 

479 

480 >>> from pytz import timezone 

481 >>> tz = timezone('America/St_Johns') 

482 

483 >>> normal = datetime(2009, 9, 1) 

484 

485 >>> tz.tzname(normal) 

486 'NDT' 

487 >>> tz.tzname(normal, is_dst=False) 

488 'NDT' 

489 >>> tz.tzname(normal, is_dst=True) 

490 'NDT' 

491 

492 >>> ambiguous = datetime(2009, 10, 31, 23, 30) 

493 

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 

511 

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 ) 

525 

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 ) 

535 

536 

537def unpickler(zone, utcoffset=None, dstoffset=None, tzname=None): 

538 """Factory function for unpickling pytz tzinfo instances. 

539 

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) 

548 

549 # A StaticTzInfo - just return it 

550 if utcoffset is None: 

551 return tz 

552 

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 

568 

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 

576 

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]