1from __future__ import annotations
2
3import contextlib
4import os
5import re
6import sys
7import warnings
8
9from contextlib import contextmanager
10from pathlib import Path
11from typing import TYPE_CHECKING
12
13from pendulum.tz.exceptions import InvalidTimezone
14from pendulum.tz.timezone import UTC
15from pendulum.tz.timezone import FixedTimezone
16from pendulum.tz.timezone import Timezone
17
18
19if TYPE_CHECKING:
20 from collections.abc import Iterator
21
22
23if sys.platform == "win32":
24 import winreg
25
26_mock_local_timezone = None
27_local_timezone = None
28
29
30def get_local_timezone() -> Timezone | FixedTimezone:
31 global _local_timezone
32
33 if _mock_local_timezone is not None:
34 return _mock_local_timezone
35
36 if _local_timezone is None:
37 tz = _get_system_timezone()
38
39 _local_timezone = tz
40
41 return _local_timezone
42
43
44def set_local_timezone(mock: str | Timezone | None = None) -> None:
45 global _mock_local_timezone
46
47 _mock_local_timezone = mock
48
49
50@contextmanager
51def test_local_timezone(mock: Timezone) -> Iterator[None]:
52 set_local_timezone(mock)
53
54 yield
55
56 set_local_timezone()
57
58
59def _get_system_timezone() -> Timezone:
60 if sys.platform == "win32":
61 return _get_windows_timezone()
62 elif "darwin" in sys.platform:
63 return _get_darwin_timezone()
64
65 return _get_unix_timezone()
66
67
68if sys.platform == "win32":
69
70 def _get_windows_timezone() -> Timezone:
71 from pendulum.tz.data.windows import windows_timezones
72
73 # Windows is special. It has unique time zone names (in several
74 # meanings of the word) available, but unfortunately, they can be
75 # translated to the language of the operating system, so we need to
76 # do a backwards lookup, by going through all time zones and see which
77 # one matches.
78 handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
79
80 tz_local_key_name = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation"
81 localtz = winreg.OpenKey(handle, tz_local_key_name)
82
83 timezone_info = {}
84 size = winreg.QueryInfoKey(localtz)[1]
85 for i in range(size):
86 data = winreg.EnumValue(localtz, i)
87 timezone_info[data[0]] = data[1]
88
89 localtz.Close()
90
91 if "TimeZoneKeyName" in timezone_info:
92 # Windows 7 (and Vista?)
93
94 # For some reason this returns a string with loads of NUL bytes at
95 # least on some systems. I don't know if this is a bug somewhere, I
96 # just work around it.
97 tzkeyname = timezone_info["TimeZoneKeyName"].split("\x00", 1)[0]
98 else:
99 # Windows 2000 or XP
100
101 # This is the localized name:
102 tzwin = timezone_info["StandardName"]
103
104 # Open the list of timezones to look up the real name:
105 tz_key_name = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones"
106 tzkey = winreg.OpenKey(handle, tz_key_name)
107
108 # Now, match this value to Time Zone information
109 tzkeyname = None
110 for i in range(winreg.QueryInfoKey(tzkey)[0]):
111 subkey = winreg.EnumKey(tzkey, i)
112 sub = winreg.OpenKey(tzkey, subkey)
113
114 info = {}
115 size = winreg.QueryInfoKey(sub)[1]
116 for i in range(size):
117 data = winreg.EnumValue(sub, i)
118 info[data[0]] = data[1]
119
120 sub.Close()
121 with contextlib.suppress(KeyError):
122 # This timezone didn't have proper configuration.
123 # Ignore it.
124 if info["Std"] == tzwin:
125 tzkeyname = subkey
126 break
127
128 tzkey.Close()
129 handle.Close()
130
131 if tzkeyname is None:
132 raise LookupError("Can not find Windows timezone configuration")
133
134 timezone = windows_timezones.get(tzkeyname)
135 if timezone is None:
136 # Nope, that didn't work. Try adding "Standard Time",
137 # it seems to work a lot of times:
138 timezone = windows_timezones.get(tzkeyname + " Standard Time")
139
140 # Return what we have.
141 if timezone is None:
142 raise LookupError("Unable to find timezone " + tzkeyname)
143
144 return Timezone(timezone)
145
146else:
147
148 def _get_windows_timezone() -> Timezone:
149 raise NotImplementedError
150
151
152def _get_darwin_timezone() -> Timezone:
153 # link will be something like /usr/share/zoneinfo/America/Los_Angeles.
154 link = os.readlink("/etc/localtime")
155 tzname = link[link.rfind("zoneinfo/") + 9 :]
156
157 return Timezone(tzname)
158
159
160def _get_unix_timezone(_root: str = "/") -> Timezone:
161 tzenv = os.environ.get("TZ")
162 if tzenv:
163 with contextlib.suppress(ValueError):
164 return _tz_from_env(tzenv)
165
166 # Now look for distribution specific configuration files
167 # that contain the timezone name.
168 tzpath = Path(_root) / "etc" / "timezone"
169 if tzpath.is_file():
170 tzfile_data = tzpath.read_bytes()
171 # Issue #3 was that /etc/timezone was a zoneinfo file.
172 # That's a misconfiguration, but we need to handle it gracefully:
173 if not tzfile_data.startswith(b"TZif2"):
174 etctz = tzfile_data.strip().decode()
175 # Get rid of host definitions and comments:
176 etctz, _, _ = etctz.partition(" ")
177 etctz, _, _ = etctz.partition("#")
178 return Timezone(etctz.replace(" ", "_"))
179
180 # CentOS has a ZONE setting in /etc/sysconfig/clock,
181 # OpenSUSE has a TIMEZONE setting in /etc/sysconfig/clock and
182 # Gentoo has a TIMEZONE setting in /etc/conf.d/clock
183 # We look through these files for a timezone:
184 zone_re = re.compile(r'\s*(TIME)?ZONE\s*=\s*"([^"]+)?"')
185
186 for filename in ("etc/sysconfig/clock", "etc/conf.d/clock"):
187 tzpath = Path(_root) / filename
188 if tzpath.is_file():
189 data = tzpath.read_text().splitlines()
190 for line in data:
191 # Look for the ZONE= or TIMEZONE= setting.
192 match = zone_re.match(line)
193 if match:
194 etctz = match.group(2)
195 parts = list(reversed(etctz.replace(" ", "_").split(os.path.sep)))
196 tzpath_parts: list[str] = []
197 while parts:
198 tzpath_parts.insert(0, parts.pop(0))
199 with contextlib.suppress(InvalidTimezone):
200 return Timezone(os.path.sep.join(tzpath_parts))
201
202 # systemd distributions use symlinks that include the zone name,
203 # see manpage of localtime(5) and timedatectl(1)
204 tzpath = Path(_root) / "etc" / "localtime"
205 if tzpath.is_file() and tzpath.is_symlink():
206 parts = [p.replace(" ", "_") for p in reversed(tzpath.resolve().parts)]
207 tzpath_parts: list[str] = [] # type: ignore[no-redef]
208 while parts:
209 tzpath_parts.insert(0, parts.pop(0))
210 with contextlib.suppress(InvalidTimezone):
211 return Timezone(os.path.sep.join(tzpath_parts))
212
213 # No explicit setting existed. Use localtime
214 for filename in ("etc/localtime", "usr/local/etc/localtime"):
215 tzpath = Path(_root) / filename
216 if tzpath.is_file():
217 with tzpath.open("rb") as f:
218 return Timezone.from_file(f)
219
220 warnings.warn(
221 "Unable not find any timezone configuration, defaulting to UTC.", stacklevel=1
222 )
223
224 return UTC
225
226
227def _tz_from_env(tzenv: str) -> Timezone:
228 if tzenv[0] == ":":
229 tzenv = tzenv[1:]
230
231 # TZ specifies a file
232 if os.path.isfile(tzenv):
233 with open(tzenv, "rb") as f:
234 return Timezone.from_file(f)
235
236 # TZ specifies a zoneinfo zone.
237 try:
238 return Timezone(tzenv)
239 except ValueError:
240 raise