import logging import os import re import sys import warnings from datetime import timezone from tzlocal import utils if sys.version_info >= (3, 9): import zoneinfo # pragma: no cover else: from backports import zoneinfo # pragma: no cover _cache_tz = None _cache_tz_name = None log = logging.getLogger("tzlocal") def _get_localzone_name(_root="/"): """Tries to find the local timezone configuration. This method finds the timezone name, if it can, or it returns None. The parameter _root makes the function look for files like /etc/localtime beneath the _root directory. This is primarily used by the tests. In normal usage you call the function without parameters.""" # First try the ENV setting. tzenv = utils._tz_name_from_env() if tzenv: return tzenv # Are we under Termux on Android? if os.path.exists(os.path.join(_root, "system/bin/getprop")): log.debug("This looks like Termux") import subprocess try: androidtz = ( subprocess.check_output(["getprop", "persist.sys.timezone"]) .strip() .decode() ) return androidtz except (OSError, subprocess.CalledProcessError): # proot environment or failed to getprop log.debug("It's not termux?") pass # Now look for distribution specific configuration files # that contain the timezone name. # Stick all of them in a dict, to compare later. found_configs = {} for configfile in ("etc/timezone", "var/db/zoneinfo"): tzpath = os.path.join(_root, configfile) try: with open(tzpath) as tzfile: data = tzfile.read() log.debug(f"{tzpath} found, contents:\n {data}") etctz = data.strip("/ \t\r\n") if not etctz: # Empty file, skip continue for etctz in etctz.splitlines(): # Get rid of host definitions and comments: if " " in etctz: etctz, dummy = etctz.split(" ", 1) if "#" in etctz: etctz, dummy = etctz.split("#", 1) if not etctz: continue found_configs[tzpath] = etctz.replace(" ", "_") except (OSError, UnicodeDecodeError): # File doesn't exist or is a directory, or it's a binary file. continue # CentOS has a ZONE setting in /etc/sysconfig/clock, # OpenSUSE has a TIMEZONE setting in /etc/sysconfig/clock and # Gentoo has a TIMEZONE setting in /etc/conf.d/clock # We look through these files for a timezone: zone_re = re.compile(r"\s*ZONE\s*=\s*\"") timezone_re = re.compile(r"\s*TIMEZONE\s*=\s*\"") end_re = re.compile('"') for filename in ("etc/sysconfig/clock", "etc/conf.d/clock"): tzpath = os.path.join(_root, filename) try: with open(tzpath, "rt") as tzfile: data = tzfile.readlines() log.debug(f"{tzpath} found, contents:\n {data}") for line in data: # Look for the ZONE= setting. match = zone_re.match(line) if match is None: # No ZONE= setting. Look for the TIMEZONE= setting. match = timezone_re.match(line) if match is not None: # Some setting existed line = line[match.end() :] etctz = line[: end_re.search(line).start()] # We found a timezone found_configs[tzpath] = etctz.replace(" ", "_") except (OSError, UnicodeDecodeError): # UnicodeDecode handles when clock is symlink to /etc/localtime continue # systemd distributions use symlinks that include the zone name, # see manpage of localtime(5) and timedatectl(1) tzpath = os.path.join(_root, "etc/localtime") if os.path.exists(tzpath) and os.path.islink(tzpath): log.debug(f"{tzpath} found") etctz = os.path.realpath(tzpath) start = etctz.find("/") + 1 while start != 0: etctz = etctz[start:] try: zoneinfo.ZoneInfo(etctz) tzinfo = f"{tzpath} is a symlink to" found_configs[tzinfo] = etctz.replace(" ", "_") # Only need first valid relative path in simlink. break except zoneinfo.ZoneInfoNotFoundError: pass start = etctz.find("/") + 1 if len(found_configs) > 0: log.debug(f"{len(found_configs)} found:\n {found_configs}") # We found some explicit config of some sort! if len(found_configs) > 1: # Uh-oh, multiple configs. See if they match: unique_tzs = set() zoneinfopath = os.path.join(_root, "usr", "share", "zoneinfo") directory_depth = len(zoneinfopath.split(os.path.sep)) for tzname in found_configs.values(): # Look them up in /usr/share/zoneinfo, and find what they # really point to: path = os.path.realpath(os.path.join(zoneinfopath, *tzname.split("/"))) real_zone_name = "/".join(path.split(os.path.sep)[directory_depth:]) unique_tzs.add(real_zone_name) if len(unique_tzs) != 1: message = "Multiple conflicting time zone configurations found:\n" for key, value in found_configs.items(): message += f"{key}: {value}\n" message += "Fix the configuration, or set the time zone in a TZ environment variable.\n" raise zoneinfo.ZoneInfoNotFoundError(message) # We found exactly one config! Use it. return list(found_configs.values())[0] def _get_localzone(_root="/"): """Creates a timezone object from the timezone name. If there is no timezone config, it will try to create a file from the localtime timezone, and if there isn't one, it will default to UTC. The parameter _root makes the function look for files like /etc/localtime beneath the _root directory. This is primarily used by the tests. In normal usage you call the function without parameters.""" # First try the ENV setting. tzenv = utils._tz_from_env() if tzenv: return tzenv tzname = _get_localzone_name(_root) if tzname is None: # No explicit setting existed. Use localtime log.debug("No explicit setting existed. Use localtime") for filename in ("etc/localtime", "usr/local/etc/localtime"): tzpath = os.path.join(_root, filename) if not os.path.exists(tzpath): continue with open(tzpath, "rb") as tzfile: tz = zoneinfo.ZoneInfo.from_file(tzfile, key="local") break else: warnings.warn("Can not find any timezone configuration, defaulting to UTC.") tz = timezone.utc else: tz = zoneinfo.ZoneInfo(tzname) if _root == "/": # We are using a file in etc to name the timezone. # Verify that the timezone specified there is actually used: utils.assert_tz_offset(tz, error=False) return tz def get_localzone_name() -> str: """Get the computers configured local timezone name, if any.""" global _cache_tz_name if _cache_tz_name is None: _cache_tz_name = _get_localzone_name() return _cache_tz_name def get_localzone() -> zoneinfo.ZoneInfo: """Get the computers configured local timezone, if any.""" global _cache_tz if _cache_tz is None: _cache_tz = _get_localzone() return _cache_tz def reload_localzone() -> zoneinfo.ZoneInfo: """Reload the cached localzone. You need to call this if the timezone has changed.""" global _cache_tz_name global _cache_tz _cache_tz_name = _get_localzone_name() _cache_tz = _get_localzone() return _cache_tz