1import os
2import sys
3
4PY36 = sys.version_info < (3, 7)
5
6
7def reset_tzpath(to=None):
8 global TZPATH
9
10 tzpaths = to
11 if tzpaths is not None:
12 if isinstance(tzpaths, (str, bytes)):
13 raise TypeError(
14 f"tzpaths must be a list or tuple, "
15 + f"not {type(tzpaths)}: {tzpaths!r}"
16 )
17
18 if not all(map(os.path.isabs, tzpaths)):
19 raise ValueError(_get_invalid_paths_message(tzpaths))
20 base_tzpath = tzpaths
21 else:
22 env_var = os.environ.get("PYTHONTZPATH", None)
23 if env_var is not None:
24 base_tzpath = _parse_python_tzpath(env_var)
25 elif sys.platform != "win32":
26 base_tzpath = [
27 "/usr/share/zoneinfo",
28 "/usr/lib/zoneinfo",
29 "/usr/share/lib/zoneinfo",
30 "/etc/zoneinfo",
31 ]
32
33 base_tzpath.sort(key=lambda x: not os.path.exists(x))
34 else:
35 base_tzpath = ()
36
37 TZPATH = tuple(base_tzpath)
38
39 if TZPATH_CALLBACKS:
40 for callback in TZPATH_CALLBACKS:
41 callback(TZPATH)
42
43
44def _parse_python_tzpath(env_var):
45 if not env_var:
46 return ()
47
48 raw_tzpath = env_var.split(os.pathsep)
49 new_tzpath = tuple(filter(os.path.isabs, raw_tzpath))
50
51 # If anything has been filtered out, we will warn about it
52 if len(new_tzpath) != len(raw_tzpath):
53 import warnings
54
55 msg = _get_invalid_paths_message(raw_tzpath)
56
57 warnings.warn(
58 "Invalid paths specified in PYTHONTZPATH environment variable."
59 + msg,
60 InvalidTZPathWarning,
61 )
62
63 return new_tzpath
64
65
66def _get_invalid_paths_message(tzpaths):
67 invalid_paths = (path for path in tzpaths if not os.path.isabs(path))
68
69 prefix = "\n "
70 indented_str = prefix + prefix.join(invalid_paths)
71
72 return (
73 "Paths should be absolute but found the following relative paths:"
74 + indented_str
75 )
76
77
78if sys.version_info < (3, 8):
79
80 def _isfile(path):
81 # bpo-33721: In Python 3.8 non-UTF8 paths return False rather than
82 # raising an error. See https://bugs.python.org/issue33721
83 try:
84 return os.path.isfile(path)
85 except ValueError:
86 return False
87
88
89else:
90 _isfile = os.path.isfile
91
92
93def find_tzfile(key):
94 """Retrieve the path to a TZif file from a key."""
95 _validate_tzfile_path(key)
96 for search_path in TZPATH:
97 filepath = os.path.join(search_path, key)
98 if _isfile(filepath):
99 return filepath
100
101 return None
102
103
104_TEST_PATH = os.path.normpath(os.path.join("_", "_"))[:-1]
105
106
107def _validate_tzfile_path(path, _base=_TEST_PATH):
108 if os.path.isabs(path):
109 raise ValueError(
110 f"ZoneInfo keys may not be absolute paths, got: {path}"
111 )
112
113 # We only care about the kinds of path normalizations that would change the
114 # length of the key - e.g. a/../b -> a/b, or a/b/ -> a/b. On Windows,
115 # normpath will also change from a/b to a\b, but that would still preserve
116 # the length.
117 new_path = os.path.normpath(path)
118 if len(new_path) != len(path):
119 raise ValueError(
120 f"ZoneInfo keys must be normalized relative paths, got: {path}"
121 )
122
123 resolved = os.path.normpath(os.path.join(_base, new_path))
124 if not resolved.startswith(_base):
125 raise ValueError(
126 f"ZoneInfo keys must refer to subdirectories of TZPATH, got: {path}"
127 )
128
129
130del _TEST_PATH
131
132
133def available_timezones():
134 """Returns a set containing all available time zones.
135
136 .. caution::
137
138 This may attempt to open a large number of files, since the best way to
139 determine if a given file on the time zone search path is to open it
140 and check for the "magic string" at the beginning.
141 """
142 try:
143 from importlib import resources
144 except ImportError:
145 import importlib_resources as resources
146
147 valid_zones = set()
148
149 # Start with loading from the tzdata package if it exists: this has a
150 # pre-assembled list of zones that only requires opening one file.
151 try:
152 with resources.open_text("tzdata", "zones") as f:
153 for zone in f:
154 zone = zone.strip()
155 if zone:
156 valid_zones.add(zone)
157 except (ImportError, FileNotFoundError):
158 pass
159
160 def valid_key(fpath):
161 try:
162 with open(fpath, "rb") as f:
163 return f.read(4) == b"TZif"
164 except Exception: # pragma: nocover
165 return False
166
167 for tz_root in TZPATH:
168 if not os.path.exists(tz_root):
169 continue
170
171 for root, dirnames, files in os.walk(tz_root):
172 if root == tz_root:
173 # right/ and posix/ are special directories and shouldn't be
174 # included in the output of available zones
175 if "right" in dirnames:
176 dirnames.remove("right")
177 if "posix" in dirnames:
178 dirnames.remove("posix")
179
180 for file in files:
181 fpath = os.path.join(root, file)
182
183 key = os.path.relpath(fpath, start=tz_root)
184 if os.sep != "/": # pragma: nocover
185 key = key.replace(os.sep, "/")
186
187 if not key or key in valid_zones:
188 continue
189
190 if valid_key(fpath):
191 valid_zones.add(key)
192
193 if "posixrules" in valid_zones:
194 # posixrules is a special symlink-only time zone where it exists, it
195 # should not be included in the output
196 valid_zones.remove("posixrules")
197
198 return valid_zones
199
200
201class InvalidTZPathWarning(RuntimeWarning):
202 """Warning raised if an invalid path is specified in PYTHONTZPATH."""
203
204
205TZPATH = ()
206TZPATH_CALLBACKS = []
207reset_tzpath()