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

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_epoch = datetime.utcfromtimestamp(0) 

28_datetime_cache = {0: _epoch} 

29 

30 

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 

41 

42_ttinfo_cache = {} 

43 

44 

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 

57 

58_notime = memorized_timedelta(0) 

59 

60 

61def _to_seconds(td): 

62 '''Convert a timedelta to seconds''' 

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

64 

65 

66class BaseTzInfo(tzinfo): 

67 # Overridden in subclass 

68 _utcoffset = None 

69 _tzname = None 

70 zone = None 

71 

72 def __str__(self): 

73 return self.zone 

74 

75 

76class StaticTzInfo(BaseTzInfo): 

77 '''A timezone that has a constant offset from UTC 

78 

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) 

87 

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

89 '''See datetime.tzinfo.utcoffset 

90 

91 is_dst is ignored for StaticTzInfo, and exists only to 

92 retain compatibility with DstTzInfo. 

93 ''' 

94 return self._utcoffset 

95 

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

97 '''See datetime.tzinfo.dst 

98 

99 is_dst is ignored for StaticTzInfo, and exists only to 

100 retain compatibility with DstTzInfo. 

101 ''' 

102 return _notime 

103 

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

105 '''See datetime.tzinfo.tzname 

106 

107 is_dst is ignored for StaticTzInfo, and exists only to 

108 retain compatibility with DstTzInfo. 

109 ''' 

110 return self._tzname 

111 

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) 

117 

118 def normalize(self, dt, is_dst=False): 

119 '''Correct the timezone information on the given datetime. 

120 

121 This is normally a no-op, as StaticTzInfo timezones never have 

122 ambiguous cases to correct: 

123 

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 

131 

132 The supported method of converting between timezones is to use 

133 datetime.astimezone(). Currently normalize() also works: 

134 

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) 

146 

147 def __repr__(self): 

148 return '<StaticTzInfo %r>' % (self.zone,) 

149 

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

154 

155 

156class DstTzInfo(BaseTzInfo): 

157 '''A timezone that has a variable offset from UTC 

158 

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 

164 

165 # Sorted list of DST transition times, UTC 

166 _utc_transition_times = None 

167 

168 # [(utcoffset, dstoffset, tzname)] corresponding to 

169 # _utc_transition_times entries 

170 _transition_info = None 

171 

172 zone = None 

173 

174 # Set in __init__ 

175 

176 _tzinfos = None 

177 _dst = None # DST offset 

178 

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) 

192 

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

202 

203 def normalize(self, dt): 

204 '''Correct the timezone information on the given datetime 

205 

206 If date arithmetic crosses DST boundaries, the tzinfo 

207 is not magically adjusted. This method normalizes the 

208 tzinfo to the correct one. 

209 

210 To test, first we need to do some setup 

211 

212 >>> from pytz import timezone 

213 >>> utc = timezone('UTC') 

214 >>> eastern = timezone('US/Eastern') 

215 >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)' 

216 

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. 

219 

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

224 

225 Now, if we subtract a few minutes from it, note that the timezone 

226 information has not changed. 

227 

228 >>> before = loc_dt - timedelta(minutes=10) 

229 >>> before.strftime(fmt) 

230 '2002-10-27 00:50:00 EST (-0500)' 

231 

232 But we can fix that by calling the normalize method 

233 

234 >>> before = eastern.normalize(before) 

235 >>> before.strftime(fmt) 

236 '2002-10-27 01:50:00 EDT (-0400)' 

237 

238 The supported method of converting between timezones is to use 

239 datetime.astimezone(). Currently, normalize() also works: 

240 

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

250 

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) 

257 

258 def localize(self, dt, is_dst=False): 

259 '''Convert naive time to local time. 

260 

261 This method should be used to construct localtimes, rather 

262 than passing a tzinfo argument to a datetime constructor. 

263 

264 is_dst is used to determine the correct timezone in the ambigous 

265 period at the end of daylight saving time. 

266 

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' 

279 

280 Use is_dst=None to raise an AmbiguousTimeError for ambiguous 

281 times at the end of daylight saving time 

282 

283 >>> try: 

284 ... loc_dt1 = amdam.localize(dt, is_dst=None) 

285 ... except AmbiguousTimeError: 

286 ... print('Ambiguous') 

287 Ambiguous 

288 

289 is_dst defaults to False 

290 

291 >>> amdam.localize(dt) == amdam.localize(dt, False) 

292 True 

293 

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. 

296 

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' 

307 

308 Use is_dst=None to raise a NonExistentTimeError for these skipped 

309 times. 

310 

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

319 

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) 

331 

332 if len(possible_loc_dt) == 1: 

333 return possible_loc_dt.pop() 

334 

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) 

342 

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) 

349 

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) 

356 

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

358 # is an ambiguous case occuring during the end-of-DST transition. 

359 

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) 

364 

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 ] 

370 

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] 

374 

375 if len(filtered_possible_loc_dt) == 0: 

376 filtered_possible_loc_dt = list(possible_loc_dt) 

377 

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

395 

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

397 '''See datetime.tzinfo.utcoffset 

398 

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

400 transitions. 

401 

402 >>> from pytz import timezone 

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

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

405 

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

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

408 

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

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

411 

412 >>> try: 

413 ... tz.utcoffset(ambiguous) 

414 ... except AmbiguousTimeError: 

415 ... print('Ambiguous') 

416 Ambiguous 

417 

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 

426 

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

428 '''See datetime.tzinfo.dst 

429 

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

431 transitions. 

432 

433 >>> from pytz import timezone 

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

435 

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

437 

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' 

444 

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

446 

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 

456 

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 

465 

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

467 '''See datetime.tzinfo.tzname 

468 

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

470 transitions. 

471 

472 >>> from pytz import timezone 

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

474 

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

476 

477 >>> tz.tzname(normal) 

478 'NDT' 

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

480 'NDT' 

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

482 'NDT' 

483 

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

485 

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 

503 

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 ) 

517 

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 ) 

527 

528 

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

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

531 

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) 

540 

541 # A StaticTzInfo - just return it 

542 if utcoffset is None: 

543 return tz 

544 

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 

560 

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 

568 

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]