1"""This is an implementation of functions that interact with the tzdata module.
2
3It will only live as long as :mod:`dateutil.zoneinfo` lives; when the
4``dateutil.zoneinfo`` module is removed, this can all move under :mod:`tz`.
5"""
6
7import importlib.resources
8from typing import BinaryIO, TextIO
9
10_TZDATA_LOAD_EXCEPTIONS = (
11 ImportError,
12 FileNotFoundError,
13 UnicodeEncodeError,
14 IsADirectoryError,
15 ValueError,
16)
17
18
19def _open_text(package: str, resource: str) -> TextIO:
20 return (
21 importlib.resources.files(package)
22 .joinpath(resource)
23 .open("r", encoding="utf-8")
24 )
25
26
27def _open_binary(package: str, resource: str) -> BinaryIO:
28 return importlib.resources.files(package).joinpath(resource).open("rb")
29
30
31def _is_package_directory(package: str, resource: str) -> bool:
32 """Checks for directories before opening resources on Windows."""
33 try:
34 return importlib.resources.files(package).joinpath(resource).is_dir()
35 except ImportError:
36 return False
37
38
39def _load_tzdata(key):
40 components = key.split("/")
41 package_name = ".".join(["tzdata.zoneinfo"] + components[:-1])
42 resource_name = components[-1]
43
44 # Empty or relative path components cannot name a zone, and they produce
45 # module names that the import machinery handles inconsistently across
46 # versions, so treat them as not found up front. A key naming a directory
47 # (a subpackage) is not a zone either, and opening it raises a
48 # platform-dependent error, see CPython gh-85702.
49 if any(c in ("", ".", "..") for c in components) or _is_package_directory(
50 package_name, resource_name
51 ):
52 raise TZFileNotFound("Time zone not found: %s", zone_key=key)
53
54 try:
55 return _open_binary(package_name, resource_name)
56 except _TZDATA_LOAD_EXCEPTIONS:
57 # ImportError: package does not exist (or tzdata is not installed)
58 # FileNotFoundError: the key is not a file in the package
59 # UnicodeEncodeError: the key is not encodable as a filename
60 # IsADirectoryError: the key names a package rather than a resource
61 # ValueError: the key is not a valid path (e.g. contains a NUL byte)
62 raise TZFileNotFound("Time zone not found: %s", zone_key=key) from None
63
64
65def _load_tzdata_keys():
66 with _open_text("tzdata", "zones") as f:
67 return list(filter(None, (l.strip() for l in f)))
68
69
70class TZFileNotFound(ValueError):
71 """Indicates that a time zone file isn't present in tzdata."""
72
73 def __init__(self, msg: str, *args: object, zone_key: str) -> None:
74 super().__init__(msg % zone_key, *args)
75 self.zone_key = zone_key