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 contextlib
8import importlib
9import io
10import os
11import pkgutil
12import sys
13
14import six
15
16if six.PY2:
17 _TZDATA_LOAD_EXCEPTIONS = (
18 ImportError,
19 IOError,
20 UnicodeEncodeError,
21 ValueError,
22 )
23else:
24 _TZDATA_LOAD_EXCEPTIONS = (
25 ImportError,
26 FileNotFoundError,
27 UnicodeEncodeError,
28 IsADirectoryError,
29 ValueError,
30 )
31
32if sys.version_info < (3, 8):
33 _nullcontext = getattr(contextlib, "nullcontext", None)
34 if _nullcontext is None:
35
36 @contextlib.contextmanager
37 def _nullcontext(v):
38 yield v
39
40 def _get_data(package, resource):
41 # pkgutil.get_data returns None rather than raising when the package
42 # cannot be found (and on Python 2 raises AttributeError if the
43 # package has been blocked by setting sys.modules[name] = None), so
44 # import the package first to get the ImportError the callers expect
45 # from the importlib.resources versions of these functions.
46 importlib.import_module(package)
47
48 pkg_data = pkgutil.get_data(package, resource)
49 if pkg_data is None:
50 raise ImportError("No package named %r" % package)
51
52 return pkg_data
53
54 def _open_text(package, resource):
55 str_package_data = _get_data(package, resource).decode("utf-8")
56
57 return _nullcontext(io.StringIO(str_package_data))
58
59 def _open_binary(package, resource):
60 return io.BytesIO(_get_data(package, resource))
61
62else:
63 import importlib.resources
64
65 if sys.version_info >= (3, 9):
66
67 def _open_text(package, resource):
68 return (
69 importlib.resources.files(package)
70 .joinpath(resource)
71 .open("r", encoding="utf-8")
72 )
73
74 def _open_binary(package, resource):
75 return (
76 importlib.resources.files(package).joinpath(resource).open("rb")
77 )
78
79 else:
80 _open_text = importlib.resources.open_text
81 _open_binary = importlib.resources.open_binary
82
83
84def _is_package_directory(package, resource):
85 """Whether ``resource`` names a directory inside ``package``.
86
87 Opening a directory raises IsADirectoryError on POSIX but PermissionError
88 on Windows, and on Python < 3.9 there is no portable way to ask the
89 resource API, so check the package's search path directly (the tzdata
90 package is a regular directory-backed package).
91 """
92 try:
93 pkg = importlib.import_module(package)
94 except ImportError:
95 return False
96
97 for base in getattr(pkg, "__path__", ()):
98 if os.path.isdir(os.path.join(base, resource)):
99 return True
100
101 return False
102
103
104def _load_tzdata(key):
105 components = key.split("/")
106 package_name = ".".join(["tzdata.zoneinfo"] + components[:-1])
107 resource_name = components[-1]
108
109 # Empty or relative path components cannot name a zone, and they produce
110 # module names that the import machinery handles inconsistently across
111 # versions, so treat them as not found up front. A key naming a directory
112 # (a subpackage) is not a zone either, and opening it raises a
113 # platform-dependent error, see CPython gh-85702.
114 if any(c in ("", ".", "..") for c in components) or _is_package_directory(
115 package_name, resource_name
116 ):
117 raise TZFileNotFound("Time zone not found: %s", zone_key=key)
118
119 try:
120 return _open_binary(package_name, resource_name)
121 except _TZDATA_LOAD_EXCEPTIONS:
122 # ImportError: package does not exist (or tzdata is not installed)
123 # FileNotFoundError: the key is not a file in the package
124 # UnicodeEncodeError: the key is not encodable as a filename
125 # IsADirectoryError: the key names a package rather than a resource
126 # ValueError: the key is not a valid path (e.g. contains a NUL byte)
127 six.raise_from(
128 TZFileNotFound("Time zone not found: %s", zone_key=key), None
129 )
130
131
132def _load_tzdata_keys():
133 with _open_text("tzdata", "zones") as f:
134 return list(filter(None, (l.strip() for l in f)))
135
136
137class TZFileNotFound(ValueError):
138 """Indicates that a time zone file isn't present in tzdata."""
139
140 def __init__(self, msg, *args, **kwargs):
141 # TODO: Use keyword-only argument when this is Python 3-only
142 zone_key = kwargs.pop("zone_key", None)
143 if zone_key is None: # pragma: nocover
144 raise TypeError("Required keyword argument missing: zone_key")
145
146 super(TZFileNotFound, self).__init__(msg % zone_key, *args)
147 self.zone_key = zone_key