Coverage for /usr/lib/python3/dist-packages/pytz/__init__.py: 42%

216 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2025-06-14 15:25 +0200

1''' 

2datetime.tzinfo timezone definitions generated from the 

3Olson timezone database: 

4 

5 ftp://elsie.nci.nih.gov/pub/tz*.tar.gz 

6 

7See the datetime section of the Python Library Reference for information 

8on how to use these modules. 

9''' 

10 

11import sys 

12import datetime 

13import os.path 

14import pathlib 

15import re 

16import zoneinfo 

17 

18from pytz.exceptions import AmbiguousTimeError 

19from pytz.exceptions import InvalidTimeError 

20from pytz.exceptions import NonExistentTimeError 

21from pytz.exceptions import UnknownTimeZoneError 

22from pytz.lazy import LazyDict, LazyList, LazySet # noqa 

23from pytz.tzinfo import unpickler, BaseTzInfo 

24from pytz.tzfile import build_tzinfo 

25 

26 

27def _read_olson_version() -> str: 

28 tzdata_zi = pathlib.Path("/usr/share/zoneinfo/tzdata.zi") 

29 with tzdata_zi.open(encoding="utf-8") as tzdata_zi_file: 

30 line = tzdata_zi_file.readline() 

31 match = re.match(r"^#\s*version\s*([0-9a-z]*)\s*$", line) 

32 if not match: 

33 return "unknown" 

34 return match.group(1) 

35 

36 

37# The IANA (nee Olson) database is updated several times a year. 

38OLSON_VERSION = _read_olson_version() 

39VERSION = '2024.1' # pip compatible version number. 

40__version__ = VERSION 

41 

42OLSEN_VERSION = OLSON_VERSION # Old releases had this misspelling 

43 

44__all__ = [ 

45 'timezone', 'utc', 'country_timezones', 'country_names', 

46 'AmbiguousTimeError', 'InvalidTimeError', 

47 'NonExistentTimeError', 'UnknownTimeZoneError', 

48 'all_timezones', 'all_timezones_set', 

49 'common_timezones', 'common_timezones_set', 

50 'BaseTzInfo', 'FixedOffset', 

51] 

52 

53 

54if sys.version_info[0] > 2: # Python 3.x 

55 

56 # Python 3.x doesn't have unicode(), making writing code 

57 # for Python 2.3 and Python 3.x a pain. 

58 unicode = str 

59 

60 def ascii(s): 

61 r""" 

62 >>> ascii('Hello') 

63 'Hello' 

64 >>> ascii('\N{TRADE MARK SIGN}') #doctest: +IGNORE_EXCEPTION_DETAIL 

65 Traceback (most recent call last): 

66 ... 

67 UnicodeEncodeError: ... 

68 """ 

69 if type(s) == bytes: 

70 s = s.decode('ASCII') 

71 else: 

72 s.encode('ASCII') # Raise an exception if not ASCII 

73 return s # But the string - not a byte string. 

74 

75else: # Python 2.x 

76 

77 def ascii(s): 

78 r""" 

79 >>> ascii('Hello') 

80 'Hello' 

81 >>> ascii(u'Hello') 

82 'Hello' 

83 >>> ascii(u'\N{TRADE MARK SIGN}') #doctest: +IGNORE_EXCEPTION_DETAIL 

84 Traceback (most recent call last): 

85 ... 

86 UnicodeEncodeError: ... 

87 """ 

88 return s.encode('ASCII') 

89 

90 

91def open_resource(name): 

92 """Open a resource from the zoneinfo subdir for reading. 

93 

94 Uses the pkg_resources module if available and no standard file 

95 found at the calculated location. 

96 

97 It is possible to specify different location for zoneinfo 

98 subdir by using the PYTZ_TZDATADIR environment variable. 

99 """ 

100 name_parts = name.lstrip('/').split('/') 

101 for part in name_parts: 

102 if part == os.path.pardir or os.sep in part: 

103 raise ValueError('Bad path segment: %r' % part) 

104 zoneinfo_dir = os.environ.get('PYTZ_TZDATADIR', None) 

105 if zoneinfo_dir is not None: 

106 filename = os.path.join(zoneinfo_dir, *name_parts) 

107 else: 

108 filename = os.path.join('/usr','share', 

109 'zoneinfo', *name_parts) 

110 if not os.path.exists(filename): 

111 # http://bugs.launchpad.net/bugs/383171 - we avoid using this 

112 # unless absolutely necessary to help when a broken version of 

113 # pkg_resources is installed. 

114 try: 

115 from pkg_resources import resource_stream 

116 except ImportError: 

117 resource_stream = None 

118 

119 if resource_stream is not None: 

120 return resource_stream(__name__, 'zoneinfo/' + name) 

121 return open(filename, 'rb') 

122 

123 

124def resource_exists(name): 

125 """Return true if the given resource exists""" 

126 try: 

127 if os.environ.get('PYTZ_SKIPEXISTSCHECK', ''): 

128 # In "standard" distributions, we can assume that 

129 # all the listed timezones are present. As an 

130 # import-speed optimization, you can set the 

131 # PYTZ_SKIPEXISTSCHECK flag to skip checking 

132 # for the presence of the resource file on disk. 

133 return True 

134 open_resource(name).close() 

135 return True 

136 except IOError: 

137 return False 

138 

139 

140_tzinfo_cache = {} 

141 

142 

143def timezone(zone): 

144 r''' Return a datetime.tzinfo implementation for the given timezone 

145 

146 >>> from datetime import datetime, timedelta 

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

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

149 >>> eastern.zone 

150 'US/Eastern' 

151 >>> timezone(unicode('US/Eastern')) is eastern 

152 True 

153 >>> utc_dt = datetime(2002, 10, 27, 6, 0, 0, tzinfo=utc) 

154 >>> loc_dt = utc_dt.astimezone(eastern) 

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

156 >>> loc_dt.strftime(fmt) 

157 '2002-10-27 01:00:00 EST (-0500)' 

158 >>> (loc_dt - timedelta(minutes=10)).strftime(fmt) 

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

160 >>> eastern.normalize(loc_dt - timedelta(minutes=10)).strftime(fmt) 

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

162 >>> (loc_dt + timedelta(minutes=10)).strftime(fmt) 

163 '2002-10-27 01:10:00 EST (-0500)' 

164 

165 Raises UnknownTimeZoneError if passed an unknown zone. 

166 

167 >>> try: 

168 ... timezone('Asia/Shangri-La') 

169 ... except UnknownTimeZoneError: 

170 ... print('Unknown') 

171 Unknown 

172 

173 >>> try: 

174 ... timezone(unicode('\N{TRADE MARK SIGN}')) 

175 ... except UnknownTimeZoneError: 

176 ... print('Unknown') 

177 Unknown 

178 

179 ''' 

180 if zone is None: 

181 raise UnknownTimeZoneError(None) 

182 

183 if zone.upper() == 'UTC': 

184 return utc 

185 

186 try: 

187 zone = ascii(zone) 

188 except UnicodeEncodeError: 

189 # All valid timezones are ASCII 

190 raise UnknownTimeZoneError(zone) 

191 

192 zone = _case_insensitive_zone_lookup(_unmunge_zone(zone)) 

193 if zone not in _tzinfo_cache: 

194 if zone in all_timezones_set: # noqa 

195 fp = open_resource(zone) 

196 try: 

197 _tzinfo_cache[zone] = build_tzinfo(zone, fp) 

198 finally: 

199 fp.close() 

200 else: 

201 raise UnknownTimeZoneError(zone) 

202 

203 return _tzinfo_cache[zone] 

204 

205 

206def _unmunge_zone(zone): 

207 """Undo the time zone name munging done by older versions of pytz.""" 

208 return zone.replace('_plus_', '+').replace('_minus_', '-') 

209 

210 

211_all_timezones_lower_to_standard = None 

212 

213 

214def _case_insensitive_zone_lookup(zone): 

215 """case-insensitively matching timezone, else return zone unchanged""" 

216 global _all_timezones_lower_to_standard 

217 if _all_timezones_lower_to_standard is None: 

218 _all_timezones_lower_to_standard = dict((tz.lower(), tz) for tz in all_timezones) # noqa 

219 return _all_timezones_lower_to_standard.get(zone.lower()) or zone # noqa 

220 

221 

222ZERO = datetime.timedelta(0) 

223HOUR = datetime.timedelta(hours=1) 

224 

225 

226class UTC(BaseTzInfo): 

227 """UTC 

228 

229 Optimized UTC implementation. It unpickles using the single module global 

230 instance defined beneath this class declaration. 

231 """ 

232 zone = "UTC" 

233 

234 _utcoffset = ZERO 

235 _dst = ZERO 

236 _tzname = zone 

237 

238 def fromutc(self, dt): 

239 if dt.tzinfo is None: 

240 return self.localize(dt) 

241 return super(utc.__class__, self).fromutc(dt) 

242 

243 def utcoffset(self, dt): 

244 return ZERO 

245 

246 def tzname(self, dt): 

247 return "UTC" 

248 

249 def dst(self, dt): 

250 return ZERO 

251 

252 def __reduce__(self): 

253 return _UTC, () 

254 

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

256 '''Convert naive time to local time''' 

257 if dt.tzinfo is not None: 

258 raise ValueError('Not naive datetime (tzinfo is already set)') 

259 return dt.replace(tzinfo=self) 

260 

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

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

263 if dt.tzinfo is self: 

264 return dt 

265 if dt.tzinfo is None: 

266 raise ValueError('Naive time - no tzinfo set') 

267 return dt.astimezone(self) 

268 

269 def __repr__(self): 

270 return "<UTC>" 

271 

272 def __str__(self): 

273 return "UTC" 

274 

275 

276UTC = utc = UTC() # UTC is a singleton 

277 

278 

279def _UTC(): 

280 """Factory function for utc unpickling. 

281 

282 Makes sure that unpickling a utc instance always returns the same 

283 module global. 

284 

285 These examples belong in the UTC class above, but it is obscured; or in 

286 the README.rst, but we are not depending on Python 2.4 so integrating 

287 the README.rst examples with the unit tests is not trivial. 

288 

289 >>> import datetime, pickle 

290 >>> dt = datetime.datetime(2005, 3, 1, 14, 13, 21, tzinfo=utc) 

291 >>> naive = dt.replace(tzinfo=None) 

292 >>> p = pickle.dumps(dt, 1) 

293 >>> naive_p = pickle.dumps(naive, 1) 

294 >>> len(p) - len(naive_p) 

295 17 

296 >>> new = pickle.loads(p) 

297 >>> new == dt 

298 True 

299 >>> new is dt 

300 False 

301 >>> new.tzinfo is dt.tzinfo 

302 True 

303 >>> utc is UTC is timezone('UTC') 

304 True 

305 >>> utc is timezone('GMT') 

306 False 

307 """ 

308 return utc 

309 

310 

311_UTC.__safe_for_unpickling__ = True 

312 

313 

314def _p(*args): 

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

316 

317 Just a wrapper around tzinfo.unpickler to save a few bytes in each pickle 

318 by shortening the path. 

319 """ 

320 return unpickler(*args) 

321 

322 

323_p.__safe_for_unpickling__ = True 

324 

325 

326class _CountryTimezoneDict(LazyDict): 

327 """Map ISO 3166 country code to a list of timezone names commonly used 

328 in that country. 

329 

330 iso3166_code is the two letter code used to identify the country. 

331 

332 >>> def print_list(list_of_strings): 

333 ... 'We use a helper so doctests work under Python 2.3 -> 3.x' 

334 ... for s in list_of_strings: 

335 ... print(s) 

336 

337 >>> print_list(country_timezones['nz']) 

338 Pacific/Auckland 

339 Pacific/Chatham 

340 >>> print_list(country_timezones['ch']) 

341 Europe/Zurich 

342 >>> print_list(country_timezones['CH']) 

343 Europe/Zurich 

344 >>> print_list(country_timezones[unicode('ch')]) 

345 Europe/Zurich 

346 >>> print_list(country_timezones['XXX']) 

347 Traceback (most recent call last): 

348 ... 

349 KeyError: 'XXX' 

350 

351 Previously, this information was exposed as a function rather than a 

352 dictionary. This is still supported:: 

353 

354 >>> print_list(country_timezones('nz')) 

355 Pacific/Auckland 

356 Pacific/Chatham 

357 """ 

358 def __call__(self, iso3166_code): 

359 """Backwards compatibility.""" 

360 return self[iso3166_code] 

361 

362 def _fill(self): 

363 data = {} 

364 zone_tab = open_resource('zone.tab') 

365 try: 

366 for line in zone_tab: 

367 line = line.decode('UTF-8') 

368 if line.startswith('#'): 

369 continue 

370 code, coordinates, zone = line.split(None, 4)[:3] 

371 if zone not in all_timezones_set: # noqa 

372 continue 

373 try: 

374 data[code].append(zone) 

375 except KeyError: 

376 data[code] = [zone] 

377 self.data = data 

378 finally: 

379 zone_tab.close() 

380 

381 

382country_timezones = _CountryTimezoneDict() 

383 

384 

385class _CountryNameDict(LazyDict): 

386 '''Dictionary proving ISO3166 code -> English name. 

387 

388 >>> print(country_names['au']) 

389 Australia 

390 ''' 

391 def _fill(self): 

392 data = {} 

393 zone_tab = open_resource('iso3166.tab') 

394 try: 

395 for line in zone_tab.readlines(): 

396 line = line.decode('UTF-8') 

397 if line.startswith('#'): 

398 continue 

399 code, name = line.split(None, 1) 

400 data[code] = name.strip() 

401 self.data = data 

402 finally: 

403 zone_tab.close() 

404 

405 

406country_names = _CountryNameDict() 

407 

408 

409# Time-zone info based solely on fixed offsets 

410 

411class _FixedOffset(datetime.tzinfo): 

412 

413 zone = None # to match the standard pytz API 

414 

415 def __init__(self, minutes): 

416 if abs(minutes) >= 1440: 

417 raise ValueError("absolute offset is too large", minutes) 

418 self._minutes = minutes 

419 self._offset = datetime.timedelta(minutes=minutes) 

420 

421 def utcoffset(self, dt): 

422 return self._offset 

423 

424 def __reduce__(self): 

425 return FixedOffset, (self._minutes, ) 

426 

427 def dst(self, dt): 

428 return ZERO 

429 

430 def tzname(self, dt): 

431 return None 

432 

433 def __repr__(self): 

434 return 'pytz.FixedOffset(%d)' % self._minutes 

435 

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

437 '''Convert naive time to local time''' 

438 if dt.tzinfo is not None: 

439 raise ValueError('Not naive datetime (tzinfo is already set)') 

440 return dt.replace(tzinfo=self) 

441 

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

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

444 if dt.tzinfo is self: 

445 return dt 

446 if dt.tzinfo is None: 

447 raise ValueError('Naive time - no tzinfo set') 

448 return dt.astimezone(self) 

449 

450 

451def FixedOffset(offset, _tzinfos={}): 

452 """return a fixed-offset timezone based off a number of minutes. 

453 

454 >>> one = FixedOffset(-330) 

455 >>> one 

456 pytz.FixedOffset(-330) 

457 >>> str(one.utcoffset(datetime.datetime.now())) 

458 '-1 day, 18:30:00' 

459 >>> str(one.dst(datetime.datetime.now())) 

460 '0:00:00' 

461 

462 >>> two = FixedOffset(1380) 

463 >>> two 

464 pytz.FixedOffset(1380) 

465 >>> str(two.utcoffset(datetime.datetime.now())) 

466 '23:00:00' 

467 >>> str(two.dst(datetime.datetime.now())) 

468 '0:00:00' 

469 

470 The datetime.timedelta must be between the range of -1 and 1 day, 

471 non-inclusive. 

472 

473 >>> FixedOffset(1440) 

474 Traceback (most recent call last): 

475 ... 

476 ValueError: ('absolute offset is too large', 1440) 

477 

478 >>> FixedOffset(-1440) 

479 Traceback (most recent call last): 

480 ... 

481 ValueError: ('absolute offset is too large', -1440) 

482 

483 An offset of 0 is special-cased to return UTC. 

484 

485 >>> FixedOffset(0) is UTC 

486 True 

487 

488 There should always be only one instance of a FixedOffset per timedelta. 

489 This should be true for multiple creation calls. 

490 

491 >>> FixedOffset(-330) is one 

492 True 

493 >>> FixedOffset(1380) is two 

494 True 

495 

496 It should also be true for pickling. 

497 

498 >>> import pickle 

499 >>> pickle.loads(pickle.dumps(one)) is one 

500 True 

501 >>> pickle.loads(pickle.dumps(two)) is two 

502 True 

503 """ 

504 if offset == 0: 

505 return UTC 

506 

507 info = _tzinfos.get(offset) 

508 if info is None: 

509 # We haven't seen this one before. we need to save it. 

510 

511 # Use setdefault to avoid a race condition and make sure we have 

512 # only one 

513 info = _tzinfos.setdefault(offset, _FixedOffset(offset)) 

514 

515 return info 

516 

517 

518FixedOffset.__safe_for_unpickling__ = True 

519 

520 

521def _test(): 

522 import doctest 

523 sys.path.insert(0, os.pardir) 

524 import pytz 

525 return doctest.testmod(pytz) 

526 

527def _read_timezones_from_zone_tab() -> set[str]: 

528 timezones = set() 

529 zone_tab = pathlib.Path("/usr/share/zoneinfo/zone1970.tab") 

530 for line in zone_tab.read_text(encoding="utf-8").splitlines(): 

531 if line.startswith("#"): 

532 continue 

533 timezones.add(line.split("\t")[2]) 

534 return timezones 

535 

536 

537if __name__ == '__main__': 

538 _test() 

539 

540_extra_common_timezones_set = { 

541 'Africa/Accra', 

542 'Africa/Addis_Ababa', 

543 'Africa/Asmara', 

544 'Africa/Bamako', 

545 'Africa/Bangui', 

546 'Africa/Banjul', 

547 'Africa/Blantyre', 

548 'Africa/Brazzaville', 

549 'Africa/Bujumbura', 

550 'Africa/Conakry', 

551 'Africa/Dakar', 

552 'Africa/Dar_es_Salaam', 

553 'Africa/Djibouti', 

554 'Africa/Douala', 

555 'Africa/Freetown', 

556 'Africa/Gaborone', 

557 'Africa/Harare', 

558 'Africa/Kampala', 

559 'Africa/Kigali', 

560 'Africa/Kinshasa', 

561 'Africa/Libreville', 

562 'Africa/Lome', 

563 'Africa/Luanda', 

564 'Africa/Lubumbashi', 

565 'Africa/Lusaka', 

566 'Africa/Malabo', 

567 'Africa/Maseru', 

568 'Africa/Mbabane', 

569 'Africa/Mogadishu', 

570 'Africa/Niamey', 

571 'Africa/Nouakchott', 

572 'Africa/Ouagadougou', 

573 'Africa/Porto-Novo', 

574 'America/Anguilla', 

575 'America/Antigua', 

576 'America/Aruba', 

577 'America/Atikokan', 

578 'America/Blanc-Sablon', 

579 'America/Cayman', 

580 'America/Creston', 

581 'America/Curacao', 

582 'America/Dominica', 

583 'America/Grenada', 

584 'America/Guadeloupe', 

585 'America/Kralendijk', 

586 'America/Lower_Princes', 

587 'America/Marigot', 

588 'America/Montserrat', 

589 'America/Nassau', 

590 'America/Port_of_Spain', 

591 'America/St_Barthelemy', 

592 'America/St_Kitts', 

593 'America/St_Lucia', 

594 'America/St_Thomas', 

595 'America/St_Vincent', 

596 'America/Tortola', 

597 'Antarctica/DumontDUrville', 

598 'Antarctica/McMurdo', 

599 'Antarctica/Syowa', 

600 'Antarctica/Vostok', 

601 'Arctic/Longyearbyen', 

602 'Asia/Aden', 

603 'Asia/Bahrain', 

604 'Asia/Kuala_Lumpur', 

605 'Asia/Kuwait', 

606 'Asia/Muscat', 

607 'Asia/Phnom_Penh', 

608 'Asia/Vientiane', 

609 'Atlantic/Reykjavik', 

610 'Atlantic/St_Helena', 

611 'Canada/Atlantic', 

612 'Canada/Central', 

613 'Canada/Eastern', 

614 'Canada/Mountain', 

615 'Canada/Newfoundland', 

616 'Canada/Pacific', 

617 'Europe/Amsterdam', 

618 'Europe/Bratislava', 

619 'Europe/Busingen', 

620 'Europe/Copenhagen', 

621 'Europe/Guernsey', 

622 'Europe/Isle_of_Man', 

623 'Europe/Jersey', 

624 'Europe/Ljubljana', 

625 'Europe/Luxembourg', 

626 'Europe/Mariehamn', 

627 'Europe/Monaco', 

628 'Europe/Oslo', 

629 'Europe/Podgorica', 

630 'Europe/San_Marino', 

631 'Europe/Sarajevo', 

632 'Europe/Skopje', 

633 'Europe/Stockholm', 

634 'Europe/Vaduz', 

635 'Europe/Vatican', 

636 'Europe/Zagreb', 

637 'GMT', 

638 'Indian/Antananarivo', 

639 'Indian/Christmas', 

640 'Indian/Cocos', 

641 'Indian/Comoro', 

642 'Indian/Kerguelen', 

643 'Indian/Mahe', 

644 'Indian/Mayotte', 

645 'Indian/Reunion', 

646 'Pacific/Chuuk', 

647 'Pacific/Funafuti', 

648 'Pacific/Majuro', 

649 'Pacific/Midway', 

650 'Pacific/Pohnpei', 

651 'Pacific/Saipan', 

652 'Pacific/Wake', 

653 'Pacific/Wallis', 

654 'US/Alaska', 

655 'US/Arizona', 

656 'US/Central', 

657 'US/Eastern', 

658 'US/Hawaii', 

659 'US/Mountain', 

660 'US/Pacific', 

661 'UTC'} 

662 

663all_timezones_set = zoneinfo.available_timezones() - {"Factory", "localtime"} 

664common_timezones_set = (_read_timezones_from_zone_tab() | _extra_common_timezones_set) & all_timezones_set 

665 

666all_timezones = sorted(all_timezones_set) 

667common_timezones = sorted(common_timezones_set)