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

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

127 statements  

1import logging 

2import os 

3import re 

4import sys 

5import warnings 

6from datetime import timezone 

7 

8from tzlocal import utils 

9 

10if sys.version_info >= (3, 9): 

11 import zoneinfo # pragma: no cover 

12else: 

13 from backports import zoneinfo # pragma: no cover 

14 

15_cache_tz = None 

16_cache_tz_name = None 

17 

18log = logging.getLogger("tzlocal") 

19 

20 

21def _get_localzone_name(_root="/"): 

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

23 

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

25 

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

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

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

29 

30 # First try the ENV setting. 

31 tzenv = utils._tz_name_from_env() 

32 if tzenv: 

33 return tzenv 

34 

35 # Are we under Termux on Android? 

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

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

38 

39 import subprocess 

40 

41 try: 

42 androidtz = ( 

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

44 .strip() 

45 .decode() 

46 ) 

47 return androidtz 

48 except (OSError, subprocess.CalledProcessError): 

49 # proot environment or failed to getprop 

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

51 pass 

52 

53 # Now look for distribution specific configuration files 

54 # that contain the timezone name. 

55 

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

57 found_configs = {} 

58 

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

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

61 try: 

62 with open(tzpath) as tzfile: 

63 data = tzfile.read() 

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

65 

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

67 if not etctz: 

68 # Empty file, skip 

69 continue 

70 for etctz in etctz.splitlines(): 

71 # Get rid of host definitions and comments: 

72 if " " in etctz: 

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

74 if "#" in etctz: 

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

76 if not etctz: 

77 continue 

78 

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

80 

81 except (OSError, UnicodeDecodeError): 

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

83 continue 

84 

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

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

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

88 # We look through these files for a timezone: 

89 

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

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

92 end_re = re.compile('"') 

93 

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

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

96 try: 

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

98 data = tzfile.readlines() 

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

100 

101 for line in data: 

102 # Look for the ZONE= setting. 

103 match = zone_re.match(line) 

104 if match is None: 

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

106 match = timezone_re.match(line) 

107 if match is not None: 

108 # Some setting existed 

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

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

111 

112 # We found a timezone 

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

114 

115 except (OSError, UnicodeDecodeError): 

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

117 continue 

118 

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

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

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

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

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

124 etctz = os.path.realpath(tzpath) 

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

126 while start != 0: 

127 etctz = etctz[start:] 

128 try: 

129 zoneinfo.ZoneInfo(etctz) 

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

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

132 # Only need first valid relative path in simlink. 

133 break 

134 except zoneinfo.ZoneInfoNotFoundError: 

135 pass 

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

137 

138 if len(found_configs) > 0: 

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

140 # We found some explicit config of some sort! 

141 if len(found_configs) > 1: 

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

143 unique_tzs = set() 

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

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

146 

147 for tzname in found_configs.values(): 

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

149 # really point to: 

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

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

152 unique_tzs.add(real_zone_name) 

153 

154 if len(unique_tzs) != 1: 

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

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

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

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

159 raise zoneinfo.ZoneInfoNotFoundError(message) 

160 

161 # We found exactly one config! Use it. 

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

163 

164 

165def _get_localzone(_root="/"): 

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

167 

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

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

170 

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

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

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

174 

175 # First try the ENV setting. 

176 tzenv = utils._tz_from_env() 

177 if tzenv: 

178 return tzenv 

179 

180 tzname = _get_localzone_name(_root) 

181 if tzname is None: 

182 # No explicit setting existed. Use localtime 

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

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

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

186 

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

188 continue 

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

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

191 break 

192 else: 

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

194 tz = timezone.utc 

195 else: 

196 tz = zoneinfo.ZoneInfo(tzname) 

197 

198 if _root == "/": 

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

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

201 utils.assert_tz_offset(tz, error=False) 

202 return tz 

203 

204 

205def get_localzone_name() -> str: 

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

207 global _cache_tz_name 

208 if _cache_tz_name is None: 

209 _cache_tz_name = _get_localzone_name() 

210 

211 return _cache_tz_name 

212 

213 

214def get_localzone() -> zoneinfo.ZoneInfo: 

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

216 

217 global _cache_tz 

218 if _cache_tz is None: 

219 _cache_tz = _get_localzone() 

220 

221 return _cache_tz 

222 

223 

224def reload_localzone() -> zoneinfo.ZoneInfo: 

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

226 global _cache_tz_name 

227 global _cache_tz 

228 _cache_tz_name = _get_localzone_name() 

229 _cache_tz = _get_localzone() 

230 

231 return _cache_tz