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