| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144 |
- 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
|