Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/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

178 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 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 

532def 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]