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