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
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
10if sys.version_info >= (3, 9):
11 import zoneinfo # pragma: no cover
12else:
13 from backports import zoneinfo # pragma: no cover
15_cache_tz = None
16_cache_tz_name = None
18log = logging.getLogger("tzlocal")
21def _get_localzone_name(_root="/"):
22 """Tries to find the local timezone configuration.
24 This method finds the timezone name, if it can, or it returns None.
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."""
30 # First try the ENV setting.
31 tzenv = utils._tz_name_from_env()
32 if tzenv:
33 return tzenv
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")
39 import subprocess
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
53 # Now look for distribution specific configuration files
54 # that contain the timezone name.
56 # Stick all of them in a dict, to compare later.
57 found_configs = {}
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}")
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
79 found_configs[tzpath] = etctz.replace(" ", "_")
81 except (OSError, UnicodeDecodeError):
82 # File doesn't exist or is a directory, or it's a binary file.
83 continue
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:
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('"')
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}")
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()]
112 # We found a timezone
113 found_configs[tzpath] = etctz.replace(" ", "_")
115 except (OSError, UnicodeDecodeError):
116 # UnicodeDecode handles when clock is symlink to /etc/localtime
117 continue
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
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))
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)
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)
161 # We found exactly one config! Use it.
162 return list(found_configs.values())[0]
165def _get_localzone(_root="/"):
166 """Creates a timezone object from the timezone name.
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.
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."""
175 # First try the ENV setting.
176 tzenv = utils._tz_from_env()
177 if tzenv:
178 return tzenv
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)
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)
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
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()
211 return _cache_tz_name
214def get_localzone() -> zoneinfo.ZoneInfo:
215 """Get the computers configured local timezone, if any."""
217 global _cache_tz
218 if _cache_tz is None:
219 _cache_tz = _get_localzone()
221 return _cache_tz
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()
231 return _cache_tz