codekingpro/portable-devtools
114k
1import os2import sysconfig3 4 5def _reset_tzpath(to=None, stacklevel=4):6 global TZPATH7 8 tzpaths = to9 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 tzpaths = [os.fspath(p) for p in tzpaths]17 if not all(isinstance(p, str) for p in tzpaths):18 raise TypeError(19 "All elements of a tzpath sequence must be strings or "20 "os.PathLike objects which convert to strings."21 )22 23 if not all(map(os.path.isabs, tzpaths)):24 raise ValueError(_get_invalid_paths_message(tzpaths))25 base_tzpath = tzpaths26 else:27 env_var = os.environ.get("PYTHONTZPATH", None)28 if env_var is None:29 env_var = sysconfig.get_config_var("TZPATH")30 base_tzpath = _parse_python_tzpath(env_var, stacklevel)31 32 TZPATH = tuple(base_tzpath)33 34 35def reset_tzpath(to=None):36 """Reset global TZPATH."""37 # We need `_reset_tzpath` helper function because it produces a warning,38 # it is used as both a module-level call and a public API.39 # This is how we equalize the stacklevel for both calls.40 _reset_tzpath(to)41 42 43def _parse_python_tzpath(env_var, stacklevel):44 if not env_var:45 return ()46 47 raw_tzpath = env_var.split(os.pathsep)48 new_tzpath = tuple(filter(os.path.isabs, raw_tzpath))49 50 # If anything has been filtered out, we will warn about it51 if len(new_tzpath) != len(raw_tzpath):52 import warnings53 54 msg = _get_invalid_paths_message(raw_tzpath)55 56 warnings.warn(57 "Invalid paths specified in PYTHONTZPATH environment variable. "58 + msg,59 InvalidTZPathWarning,60 stacklevel=stacklevel,61 )62 63 return new_tzpath64 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_str75 )76 77 78def find_tzfile(key):79 """Retrieve the path to a TZif file from a key."""80 _validate_tzfile_path(key)81 for search_path in TZPATH:82 filepath = os.path.join(search_path, key)83 if os.path.isfile(filepath):84 return filepath85 86 return None87 88 89_TEST_PATH = os.path.normpath(os.path.join("_", "_"))[:-1]90 91 92def _validate_tzfile_path(path, _base=_TEST_PATH):93 if os.path.isabs(path):94 raise ValueError(95 f"ZoneInfo keys may not be absolute paths, got: {path}"96 )97 98 # We only care about the kinds of path normalizations that would change the99 # length of the key - e.g. a/../b -> a/b, or a/b/ -> a/b. On Windows,100 # normpath will also change from a/b to a\b, but that would still preserve101 # the length.102 new_path = os.path.normpath(path)103 if len(new_path) != len(path):104 raise ValueError(105 f"ZoneInfo keys must be normalized relative paths, got: {path}"106 )107 108 resolved = os.path.normpath(os.path.join(_base, new_path))109 if not resolved.startswith(_base):110 raise ValueError(111 f"ZoneInfo keys must refer to subdirectories of TZPATH, got: {path}"112 )113 114 115del _TEST_PATH116 117 118def available_timezones():119 """Returns a set containing all available time zones.120 121 .. caution::122 123 This may attempt to open a large number of files, since the best way to124 determine if a given file on the time zone search path is to open it125 and check for the "magic string" at the beginning.126 """127 from importlib import resources128 129 valid_zones = set()130 131 # Start with loading from the tzdata package if it exists: this has a132 # pre-assembled list of zones that only requires opening one file.133 try:134 zones_file = resources.files("tzdata").joinpath("zones")135 with zones_file.open("r", encoding="utf-8") as f:136 for zone in f:137 zone = zone.strip()138 if zone:139 valid_zones.add(zone)140 except (ImportError, FileNotFoundError):141 pass142 143 def valid_key(fpath):144 try:145 with open(fpath, "rb") as f:146 return f.read(4) == b"TZif"147 except Exception: # pragma: nocover148 return False149 150 for tz_root in TZPATH:151 if not os.path.exists(tz_root):152 continue153 154 for root, dirnames, files in os.walk(tz_root):155 if root == tz_root:156 # right/ and posix/ are special directories and shouldn't be157 # included in the output of available zones158 if "right" in dirnames:159 dirnames.remove("right")160 if "posix" in dirnames:161 dirnames.remove("posix")162 163 for file in files:164 fpath = os.path.join(root, file)165 166 key = os.path.relpath(fpath, start=tz_root)167 if os.sep != "/": # pragma: nocover168 key = key.replace(os.sep, "/")169 170 if not key or key in valid_zones:171 continue172 173 if valid_key(fpath):174 valid_zones.add(key)175 176 if "posixrules" in valid_zones:177 # posixrules is a special symlink-only time zone where it exists, it178 # should not be included in the output179 valid_zones.remove("posixrules")180 181 return valid_zones182 183 184class InvalidTZPathWarning(RuntimeWarning):185 """Warning raised if an invalid path is specified in PYTHONTZPATH."""186 187 188TZPATH = ()189_reset_tzpath(stacklevel=5)190 