Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/tzlocal/unix.py: 63%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

137 statements  

1import logging 

2import os 

3import re 

4import sys 

5import warnings 

6from datetime import timezone 

7 

8from tzlocal import utils 

9 

10import zoneinfo 

11 

12_cache_tz = None 

13_cache_tz_name = None 

14 

15log = logging.getLogger("tzlocal") 

16 

17 

18def _get_localzone_name(_root="/"): 

19 """Tries to find the local timezone configuration. 

20 

21 This method finds the timezone name, if it can, or it returns None. 

22 

23 The parameter _root makes the function look for files like /etc/localtime 

24 beneath the _root directory. This is primarily used by the tests. 

25 In normal usage you call the function without parameters.""" 

26 

27 # First try the ENV setting. 

28 tzenv = utils._tz_name_from_env() 

29 if tzenv: 

30 return tzenv 

31 

32 # Are we under Termux on Android? 

33 if os.path.exists(os.path.join(_root, "system/bin/getprop")): 

34 log.debug("This looks like Termux") 

35 

36 import subprocess 

37 

38 try: 

39 androidtz = ( 

40 subprocess.check_output(["getprop", "persist.sys.timezone"]) 

41 .strip() 

42 .decode() 

43 ) 

44 return androidtz 

45 except (OSError, subprocess.CalledProcessError): 

46 # proot environment or failed to getprop 

47 log.debug("It's not termux?") 

48 pass 

49 

50 # Now look for distribution specific configuration files 

51 # that contain the timezone name. 

52 

53 # Stick all of them in a dict, to compare later. 

54 found_configs = {} 

55 

56 for configfile in ("etc/timezone", "var/db/zoneinfo"): 

57 tzpath = os.path.join(_root, configfile) 

58 try: 

59 with open(tzpath) as tzfile: 

60 data = tzfile.read() 

61 log.debug(f"{tzpath} found, contents:\n {data}") 

62 

63 etctz = data.strip("/ \t\r\n") 

64 if not etctz: 

65 # Empty file, skip 

66 continue 

67 for etctz in etctz.splitlines(): 

68 # Get rid of host definitions and comments: 

69 if " " in etctz: 

70 etctz, dummy = etctz.split(" ", 1) 

71 if "#" in etctz: 

72 etctz, dummy = etctz.split("#", 1) 

73 if not etctz: 

74 continue 

75 

76 found_configs[tzpath] = etctz.replace(" ", "_") 

77 

78 except (OSError, UnicodeDecodeError): 

79 # File doesn't exist or is a directory, or it's a binary file. 

80 continue 

81 

82 # CentOS has a ZONE setting in /etc/sysconfig/clock, 

83 # OpenSUSE has a TIMEZONE setting in /etc/sysconfig/clock and 

84 # Gentoo has a TIMEZONE setting in /etc/conf.d/clock 

85 # We look through these files for a timezone: 

86 

87 zone_re = re.compile(r"\s*ZONE\s*=\s*\"") 

88 timezone_re = re.compile(r"\s*TIMEZONE\s*=\s*\"") 

89 end_re = re.compile('"') 

90 

91 for filename in ("etc/sysconfig/clock", "etc/conf.d/clock"): 

92 tzpath = os.path.join(_root, filename) 

93 try: 

94 with open(tzpath, "rt") as tzfile: 

95 data = tzfile.readlines() 

96 log.debug(f"{tzpath} found, contents:\n {data}") 

97 

98 for line in data: 

99 # Look for the ZONE= setting. 

100 match = zone_re.match(line) 

101 if match is None: 

102 # No ZONE= setting. Look for the TIMEZONE= setting. 

103 match = timezone_re.match(line) 

104 if match is not None: 

105 # Some setting existed 

106 line = line[match.end() :] 

107 etctz = line[: end_re.search(line).start()] 

108 

109 # We found a timezone 

110 found_configs[tzpath] = etctz.replace(" ", "_") 

111 

112 except (OSError, UnicodeDecodeError): 

113 # UnicodeDecode handles when clock is symlink to /etc/localtime 

114 continue 

115 

116 # systemd distributions use symlinks that include the zone name, 

117 # see manpage of localtime(5) and timedatectl(1) 

118 tzpath = os.path.join(_root, "etc/localtime") 

119 if os.path.exists(tzpath) and os.path.islink(tzpath): 

120 log.debug(f"{tzpath} found") 

121 etctz = os.path.realpath(tzpath) 

122 start = etctz.find("/") + 1 

123 while start != 0: 

124 etctz = etctz[start:] 

125 try: 

126 zoneinfo.ZoneInfo(etctz) 

127 tzinfo = f"{tzpath} is a symlink to" 

128 found_configs[tzinfo] = etctz.replace(" ", "_") 

129 # Only need first valid relative path in simlink. 

130 break 

131 except zoneinfo.ZoneInfoNotFoundError: 

132 pass 

133 start = etctz.find("/") + 1 

134 

135 if len(found_configs) > 0: 

136 log.debug(f"{len(found_configs)} found:\n {found_configs}") 

137 

138 # We found some explicit config of some sort! 

139 if len(found_configs) > 1: 

140 # Uh-oh, multiple configs. See if they match: 

141 unique_tzs = _get_unique_tzs(found_configs, _root) 

142 

143 if len(unique_tzs) != 1 and "etc/timezone" in str(found_configs.keys()): 

144 # For some reason some distros are removing support for /etc/timezone,  

145 # which is bad, because that's the only place where the timezone is stated  

146 # in plain text, and what's worse, they don't delete it. So we can't trust  

147 # it now, so when we have conflicting configs, we just ignore it, with a warning. 

148 log.warning("/etc/timezone is deprecated in some distros, and no longer reliable. " 

149 "tzlocal is ignoring it, and you can likely delete it.") 

150 found_configs = {k: v for k, v in found_configs.items() if "etc/timezone" not in k} 

151 unique_tzs = _get_unique_tzs(found_configs, _root) 

152 

153 if len(unique_tzs) != 1: 

154 message = "Multiple conflicting time zone configurations found:\n" 

155 for key, value in found_configs.items(): 

156 message += f"{key}: {value}\n" 

157 message += "Fix the configuration, or set the time zone in a TZ environment variable.\n" 

158 raise zoneinfo.ZoneInfoNotFoundError(message) 

159 

160 # We found exactly one config! Use it. 

161 return list(found_configs.values())[0] 

162 

163 

164def _get_unique_tzs(found_configs, _root): 

165 unique_tzs = set() 

166 zoneinfopath = os.path.join(_root, "usr", "share", "zoneinfo") 

167 directory_depth = len(zoneinfopath.split(os.path.sep)) 

168 

169 for tzname in found_configs.values(): 

170 # Look them up in /usr/share/zoneinfo, and find what they 

171 # really point to: 

172 path = os.path.realpath(os.path.join(zoneinfopath, *tzname.split("/"))) 

173 real_zone_name = "/".join(path.split(os.path.sep)[directory_depth:]) 

174 unique_tzs.add(real_zone_name) 

175 

176 return unique_tzs 

177 

178 

179def _get_localzone(_root="/"): 

180 """Creates a timezone object from the timezone name. 

181 

182 If there is no timezone config, it will try to create a file from the 

183 localtime timezone, and if there isn't one, it will default to UTC. 

184 

185 The parameter _root makes the function look for files like /etc/localtime 

186 beneath the _root directory. This is primarily used by the tests. 

187 In normal usage you call the function without parameters.""" 

188 

189 # First try the ENV setting. 

190 tzenv = utils._tz_from_env() 

191 if tzenv: 

192 return tzenv 

193 

194 tzname = _get_localzone_name(_root) 

195 if tzname is None: 

196 # No explicit setting existed. Use localtime 

197 log.debug("No explicit setting existed. Use localtime") 

198 for filename in ("etc/localtime", "usr/local/etc/localtime"): 

199 tzpath = os.path.join(_root, filename) 

200 

201 if not os.path.exists(tzpath): 

202 continue 

203 with open(tzpath, "rb") as tzfile: 

204 tz = zoneinfo.ZoneInfo.from_file(tzfile, key="local") 

205 break 

206 else: 

207 warnings.warn("Can not find any timezone configuration, defaulting to UTC.") 

208 utcname = [x for x in zoneinfo.available_timezones() if "UTC" in x] 

209 if utcname: 

210 tz = zoneinfo.ZoneInfo(utcname[0]) 

211 else: 

212 tz = timezone.utc 

213 else: 

214 tz = zoneinfo.ZoneInfo(tzname) 

215 

216 if _root == "/": 

217 # We are using a file in etc to name the timezone. 

218 # Verify that the timezone specified there is actually used: 

219 utils.assert_tz_offset(tz, error=False) 

220 return tz 

221 

222 

223def get_localzone_name() -> str: 

224 """Get the computers configured local timezone name, if any.""" 

225 global _cache_tz_name 

226 if _cache_tz_name is None: 

227 _cache_tz_name = _get_localzone_name() 

228 

229 return _cache_tz_name 

230 

231 

232def get_localzone() -> zoneinfo.ZoneInfo: 

233 """Get the computers configured local timezone, if any.""" 

234 

235 global _cache_tz 

236 if _cache_tz is None: 

237 _cache_tz = _get_localzone() 

238 

239 return _cache_tz 

240 

241 

242def reload_localzone() -> zoneinfo.ZoneInfo: 

243 """Reload the cached localzone. You need to call this if the timezone has changed.""" 

244 global _cache_tz_name 

245 global _cache_tz 

246 _cache_tz_name = _get_localzone_name() 

247 _cache_tz = _get_localzone() 

248 

249 return _cache_tz