win32.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. import logging
  2. from datetime import datetime
  3. try:
  4. import _winreg as winreg
  5. except ImportError:
  6. import winreg
  7. import zoneinfo
  8. from tzlocal import utils
  9. from tzlocal.windows_tz import win_tz
  10. _cache_tz = None
  11. _cache_tz_name = None
  12. log = logging.getLogger("tzlocal")
  13. def valuestodict(key):
  14. """Convert a registry key's values to a dictionary."""
  15. result = {}
  16. size = winreg.QueryInfoKey(key)[1]
  17. for i in range(size):
  18. data = winreg.EnumValue(key, i)
  19. result[data[0]] = data[1]
  20. return result
  21. def _get_dst_info(tz):
  22. # Find the offset for when it doesn't have DST:
  23. dst_offset = std_offset = None
  24. has_dst = False
  25. year = datetime.now().year
  26. for dt in (datetime(year, 1, 1), datetime(year, 6, 1)):
  27. if tz.dst(dt).total_seconds() == 0.0:
  28. # OK, no DST during winter, get this offset
  29. std_offset = tz.utcoffset(dt).total_seconds()
  30. else:
  31. has_dst = True
  32. return has_dst, std_offset, dst_offset
  33. def _get_localzone_name():
  34. # Windows is special. It has unique time zone names (in several
  35. # meanings of the word) available, but unfortunately, they can be
  36. # translated to the language of the operating system, so we need to
  37. # do a backwards lookup, by going through all time zones and see which
  38. # one matches.
  39. tzenv = utils._tz_name_from_env()
  40. if tzenv:
  41. return tzenv
  42. log.debug("Looking up time zone info from registry")
  43. handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
  44. TZLOCALKEYNAME = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation"
  45. localtz = winreg.OpenKey(handle, TZLOCALKEYNAME)
  46. keyvalues = valuestodict(localtz)
  47. localtz.Close()
  48. if "TimeZoneKeyName" in keyvalues:
  49. # Windows 7 and later
  50. # For some reason this returns a string with loads of NUL bytes at
  51. # least on some systems. I don't know if this is a bug somewhere, I
  52. # just work around it.
  53. tzkeyname = keyvalues["TimeZoneKeyName"].split("\x00", 1)[0]
  54. else:
  55. # Don't support XP any longer
  56. raise LookupError("Can not find Windows timezone configuration")
  57. timezone = win_tz.get(tzkeyname)
  58. if timezone is None:
  59. # Nope, that didn't work. Try adding "Standard Time",
  60. # it seems to work a lot of times:
  61. timezone = win_tz.get(tzkeyname + " Standard Time")
  62. # Return what we have.
  63. if timezone is None:
  64. raise zoneinfo.ZoneInfoNotFoundError(tzkeyname)
  65. if keyvalues.get("DynamicDaylightTimeDisabled", 0) == 1:
  66. # DST is disabled, so don't return the timezone name,
  67. # instead return Etc/GMT+offset
  68. tz = zoneinfo.ZoneInfo(timezone)
  69. has_dst, std_offset, dst_offset = _get_dst_info(tz)
  70. if not has_dst:
  71. # The DST is turned off in the windows configuration,
  72. # but this timezone doesn't have DST so it doesn't matter
  73. return timezone
  74. if std_offset is None:
  75. raise zoneinfo.ZoneInfoNotFoundError(
  76. f"{tzkeyname} claims to not have a non-DST time!?"
  77. )
  78. if std_offset % 3600:
  79. # I can't convert this to an hourly offset
  80. raise zoneinfo.ZoneInfoNotFoundError(
  81. f"tzlocal can't support disabling DST in the {timezone} zone."
  82. )
  83. # This has whole hours as offset, return it as Etc/GMT
  84. return f"Etc/GMT{-std_offset//3600:+.0f}"
  85. return timezone
  86. def get_localzone_name() -> str:
  87. """Get the zoneinfo timezone name that matches the Windows-configured timezone."""
  88. global _cache_tz_name
  89. if _cache_tz_name is None:
  90. _cache_tz_name = _get_localzone_name()
  91. return _cache_tz_name
  92. def get_localzone() -> zoneinfo.ZoneInfo:
  93. """Returns the zoneinfo-based tzinfo object that matches the Windows-configured timezone."""
  94. global _cache_tz
  95. if _cache_tz is None:
  96. _cache_tz = zoneinfo.ZoneInfo(get_localzone_name())
  97. if not utils._tz_name_from_env():
  98. # If the timezone does NOT come from a TZ environment variable,
  99. # verify that it's correct. If it's from the environment,
  100. # we accept it, this is so you can run tests with different timezones.
  101. utils.assert_tz_offset(_cache_tz, error=False)
  102. return _cache_tz
  103. def reload_localzone() -> zoneinfo.ZoneInfo:
  104. """Reload the cached localzone. You need to call this if the timezone has changed."""
  105. global _cache_tz
  106. global _cache_tz_name
  107. _cache_tz_name = _get_localzone_name()
  108. _cache_tz = zoneinfo.ZoneInfo(_cache_tz_name)
  109. utils.assert_tz_offset(_cache_tz, error=False)
  110. return _cache_tz