summaryrefslogtreecommitdiffstats
path: root/pendulum/tz/local_timezone.py
diff options
context:
space:
mode:
Diffstat (limited to 'pendulum/tz/local_timezone.py')
-rw-r--r--pendulum/tz/local_timezone.py257
1 files changed, 257 insertions, 0 deletions
diff --git a/pendulum/tz/local_timezone.py b/pendulum/tz/local_timezone.py
new file mode 100644
index 0000000..756105a
--- /dev/null
+++ b/pendulum/tz/local_timezone.py
@@ -0,0 +1,257 @@
+import os
+import re
+import sys
+
+from contextlib import contextmanager
+from typing import Iterator
+from typing import Optional
+from typing import Union
+
+from .timezone import Timezone
+from .timezone import TimezoneFile
+from .zoneinfo.exceptions import InvalidTimezone
+
+
+try:
+ import _winreg as winreg
+except ImportError:
+ try:
+ import winreg
+ except ImportError:
+ winreg = None
+
+
+_mock_local_timezone = None
+_local_timezone = None
+
+
+def get_local_timezone(): # type: () -> Timezone
+ global _local_timezone
+
+ if _mock_local_timezone is not None:
+ return _mock_local_timezone
+
+ if _local_timezone is None:
+ tz = _get_system_timezone()
+
+ _local_timezone = tz
+
+ return _local_timezone
+
+
+def set_local_timezone(mock=None): # type: (Optional[Union[str, Timezone]]) -> None
+ global _mock_local_timezone
+
+ _mock_local_timezone = mock
+
+
+@contextmanager
+def test_local_timezone(mock): # type: (Timezone) -> Iterator[None]
+ set_local_timezone(mock)
+
+ yield
+
+ set_local_timezone()
+
+
+def _get_system_timezone(): # type: () -> Timezone
+ if sys.platform == "win32":
+ return _get_windows_timezone()
+ elif "darwin" in sys.platform:
+ return _get_darwin_timezone()
+
+ return _get_unix_timezone()
+
+
+def _get_windows_timezone(): # type: () -> Timezone
+ from .data.windows import windows_timezones
+
+ # Windows is special. It has unique time zone names (in several
+ # meanings of the word) available, but unfortunately, they can be
+ # translated to the language of the operating system, so we need to
+ # do a backwards lookup, by going through all time zones and see which
+ # one matches.
+ handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
+
+ tz_local_key_name = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation"
+ localtz = winreg.OpenKey(handle, tz_local_key_name)
+
+ timezone_info = {}
+ size = winreg.QueryInfoKey(localtz)[1]
+ for i in range(size):
+ data = winreg.EnumValue(localtz, i)
+ timezone_info[data[0]] = data[1]
+
+ localtz.Close()
+
+ if "TimeZoneKeyName" in timezone_info:
+ # Windows 7 (and Vista?)
+
+ # For some reason this returns a string with loads of NUL bytes at
+ # least on some systems. I don't know if this is a bug somewhere, I
+ # just work around it.
+ tzkeyname = timezone_info["TimeZoneKeyName"].split("\x00", 1)[0]
+ else:
+ # Windows 2000 or XP
+
+ # This is the localized name:
+ tzwin = timezone_info["StandardName"]
+
+ # Open the list of timezones to look up the real name:
+ tz_key_name = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones"
+ tzkey = winreg.OpenKey(handle, tz_key_name)
+
+ # Now, match this value to Time Zone information
+ tzkeyname = None
+ for i in range(winreg.QueryInfoKey(tzkey)[0]):
+ subkey = winreg.EnumKey(tzkey, i)
+ sub = winreg.OpenKey(tzkey, subkey)
+
+ info = {}
+ size = winreg.QueryInfoKey(sub)[1]
+ for i in range(size):
+ data = winreg.EnumValue(sub, i)
+ info[data[0]] = data[1]
+
+ sub.Close()
+ try:
+ if info["Std"] == tzwin:
+ tzkeyname = subkey
+ break
+ except KeyError:
+ # This timezone didn't have proper configuration.
+ # Ignore it.
+ pass
+
+ tzkey.Close()
+ handle.Close()
+
+ if tzkeyname is None:
+ raise LookupError("Can not find Windows timezone configuration")
+
+ timezone = windows_timezones.get(tzkeyname)
+ if timezone is None:
+ # Nope, that didn't work. Try adding "Standard Time",
+ # it seems to work a lot of times:
+ timezone = windows_timezones.get(tzkeyname + " Standard Time")
+
+ # Return what we have.
+ if timezone is None:
+ raise LookupError("Unable to find timezone " + tzkeyname)
+
+ return Timezone(timezone)
+
+
+def _get_darwin_timezone(): # type: () -> Timezone
+ # link will be something like /usr/share/zoneinfo/America/Los_Angeles.
+ link = os.readlink("/etc/localtime")
+ tzname = link[link.rfind("zoneinfo/") + 9 :]
+
+ return Timezone(tzname)
+
+
+def _get_unix_timezone(_root="/"): # type: (str) -> Timezone
+ tzenv = os.environ.get("TZ")
+ if tzenv:
+ try:
+ return _tz_from_env(tzenv)
+ except ValueError:
+ pass
+
+ # Now look for distribution specific configuration files
+ # that contain the timezone name.
+ tzpath = os.path.join(_root, "etc/timezone")
+ if os.path.exists(tzpath):
+ with open(tzpath, "rb") as tzfile:
+ data = tzfile.read()
+
+ # Issue #3 was that /etc/timezone was a zoneinfo file.
+ # That's a misconfiguration, but we need to handle it gracefully:
+ if data[:5] != "TZif2":
+ etctz = data.strip().decode()
+ # Get rid of host definitions and comments:
+ if " " in etctz:
+ etctz, dummy = etctz.split(" ", 1)
+ if "#" in etctz:
+ etctz, dummy = etctz.split("#", 1)
+
+ return Timezone(etctz.replace(" ", "_"))
+
+ # 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)
+ if not os.path.exists(tzpath):
+ continue
+
+ with open(tzpath, "rt") as tzfile:
+ data = tzfile.readlines()
+
+ 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()]
+
+ parts = list(reversed(etctz.replace(" ", "_").split(os.path.sep)))
+ tzpath = []
+ while parts:
+ tzpath.insert(0, parts.pop(0))
+
+ try:
+ return Timezone(os.path.join(*tzpath))
+ except InvalidTimezone:
+ pass
+
+ # 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):
+ parts = list(
+ reversed(os.path.realpath(tzpath).replace(" ", "_").split(os.path.sep))
+ )
+ tzpath = []
+ while parts:
+ tzpath.insert(0, parts.pop(0))
+ try:
+ return Timezone(os.path.join(*tzpath))
+ except InvalidTimezone:
+ pass
+
+ # 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
+
+ return TimezoneFile(tzpath)
+
+ raise RuntimeError("Unable to find any timezone configuration")
+
+
+def _tz_from_env(tzenv): # type: (str) -> Timezone
+ if tzenv[0] == ":":
+ tzenv = tzenv[1:]
+
+ # TZ specifies a file
+ if os.path.exists(tzenv):
+ return TimezoneFile(tzenv)
+
+ # TZ specifies a zoneinfo zone.
+ try:
+ return Timezone(tzenv)
+ except ValueError:
+ raise