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
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
1import logging
2import os
3import re
4import sys
5import warnings
6from datetime import timezone
8from tzlocal import utils
10import zoneinfo
12_cache_tz = None
13_cache_tz_name = None
15log = logging.getLogger("tzlocal")
18def _get_localzone_name(_root="/"):
19 """Tries to find the local timezone configuration.
21 This method finds the timezone name, if it can, or it returns None.
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."""
27 # First try the ENV setting.
28 tzenv = utils._tz_name_from_env()
29 if tzenv:
30 return tzenv
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")
36 import subprocess
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
50 # Now look for distribution specific configuration files
51 # that contain the timezone name.
53 # Stick all of them in a dict, to compare later.
54 found_configs = {}
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}")
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
76 found_configs[tzpath] = etctz.replace(" ", "_")
78 except (OSError, UnicodeDecodeError):
79 # File doesn't exist or is a directory, or it's a binary file.
80 continue
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:
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('"')
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}")
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()]
109 # We found a timezone
110 found_configs[tzpath] = etctz.replace(" ", "_")
112 except (OSError, UnicodeDecodeError):
113 # UnicodeDecode handles when clock is symlink to /etc/localtime
114 continue
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
135 if len(found_configs) > 0:
136 log.debug(f"{len(found_configs)} found:\n {found_configs}")
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)
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)
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)
160 # We found exactly one config! Use it.
161 return list(found_configs.values())[0]
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))
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)
176 return unique_tzs
179def _get_localzone(_root="/"):
180 """Creates a timezone object from the timezone name.
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.
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."""
189 # First try the ENV setting.
190 tzenv = utils._tz_from_env()
191 if tzenv:
192 return tzenv
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)
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)
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
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()
229 return _cache_tz_name
232def get_localzone() -> zoneinfo.ZoneInfo:
233 """Get the computers configured local timezone, if any."""
235 global _cache_tz
236 if _cache_tz is None:
237 _cache_tz = _get_localzone()
239 return _cache_tz
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()
249 return _cache_tz