1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
|
import logging
from datetime import datetime
try:
import _winreg as winreg
except ImportError:
import winreg
import zoneinfo
from tzlocal import utils
from tzlocal.windows_tz import win_tz
_cache_tz = None
_cache_tz_name = None
log = logging.getLogger("tzlocal")
def valuestodict(key):
"""Convert a registry key's values to a dictionary."""
result = {}
size = winreg.QueryInfoKey(key)[1]
for i in range(size):
data = winreg.EnumValue(key, i)
result[data[0]] = data[1]
return result
def _get_dst_info(tz):
# Find the offset for when it doesn't have DST:
dst_offset = std_offset = None
has_dst = False
year = datetime.now().year
for dt in (datetime(year, 1, 1), datetime(year, 6, 1)):
if tz.dst(dt).total_seconds() == 0.0:
# OK, no DST during winter, get this offset
std_offset = tz.utcoffset(dt).total_seconds()
else:
has_dst = True
return has_dst, std_offset, dst_offset
def _get_localzone_name():
# 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.
tzenv = utils._tz_name_from_env()
if tzenv:
return tzenv
log.debug("Looking up time zone info from registry")
handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
TZLOCALKEYNAME = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation"
localtz = winreg.OpenKey(handle, TZLOCALKEYNAME)
keyvalues = valuestodict(localtz)
localtz.Close()
if "TimeZoneKeyName" in keyvalues:
# Windows 7 and later
# 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 = keyvalues["TimeZoneKeyName"].split("\x00", 1)[0]
else:
# Don't support XP any longer
raise LookupError("Can not find Windows timezone configuration")
timezone = win_tz.get(tzkeyname)
if timezone is None:
# Nope, that didn't work. Try adding "Standard Time",
# it seems to work a lot of times:
timezone = win_tz.get(tzkeyname + " Standard Time")
# Return what we have.
if timezone is None:
raise zoneinfo.ZoneInfoNotFoundError(tzkeyname)
if keyvalues.get("DynamicDaylightTimeDisabled", 0) == 1:
# DST is disabled, so don't return the timezone name,
# instead return Etc/GMT+offset
tz = zoneinfo.ZoneInfo(timezone)
has_dst, std_offset, dst_offset = _get_dst_info(tz)
if not has_dst:
# The DST is turned off in the windows configuration,
# but this timezone doesn't have DST so it doesn't matter
return timezone
if std_offset is None:
raise zoneinfo.ZoneInfoNotFoundError(
f"{tzkeyname} claims to not have a non-DST time!?"
)
if std_offset % 3600:
# I can't convert this to an hourly offset
raise zoneinfo.ZoneInfoNotFoundError(
f"tzlocal can't support disabling DST in the {timezone} zone."
)
# This has whole hours as offset, return it as Etc/GMT
return f"Etc/GMT{-std_offset//3600:+.0f}"
return timezone
def get_localzone_name() -> str:
"""Get the zoneinfo timezone name that matches the Windows-configured timezone."""
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:
"""Returns the zoneinfo-based tzinfo object that matches the Windows-configured timezone."""
global _cache_tz
if _cache_tz is None:
_cache_tz = zoneinfo.ZoneInfo(get_localzone_name())
if not utils._tz_name_from_env():
# If the timezone does NOT come from a TZ environment variable,
# verify that it's correct. If it's from the environment,
# we accept it, this is so you can run tests with different timezones.
utils.assert_tz_offset(_cache_tz, error=False)
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
global _cache_tz_name
_cache_tz_name = _get_localzone_name()
_cache_tz = zoneinfo.ZoneInfo(_cache_tz_name)
utils.assert_tz_offset(_cache_tz, error=False)
return _cache_tz
|