1from __future__ import annotations
2
3import contextlib
4import os
5import re
6import sys
7import warnings
8
9from contextlib import contextmanager
10from typing import TYPE_CHECKING
11from typing import cast
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 = os.path.join(_root, "etc/timezone")
169 if os.path.isfile(tzpath):
170 with open(tzpath, "rb") as tzfile:
171 tzfile_data = tzfile.read()
172
173 # Issue #3 was that /etc/timezone was a zoneinfo file.
174 # That's a misconfiguration, but we need to handle it gracefully:
175 if tzfile_data[:5] != b"TZif2":
176 etctz = tzfile_data.strip().decode()
177 # Get rid of host definitions and comments:
178 if " " in etctz:
179 etctz, dummy = etctz.split(" ", 1)
180 if "#" in etctz:
181 etctz, dummy = etctz.split("#", 1)
182
183 return Timezone(etctz.replace(" ", "_"))
184
185 # CentOS has a ZONE setting in /etc/sysconfig/clock,
186 # OpenSUSE has a TIMEZONE setting in /etc/sysconfig/clock and
187 # Gentoo has a TIMEZONE setting in /etc/conf.d/clock
188 # We look through these files for a timezone:
189 zone_re = re.compile(r'\s*ZONE\s*=\s*"')
190 timezone_re = re.compile(r'\s*TIMEZONE\s*=\s*"')
191 end_re = re.compile('"')
192
193 for filename in ("etc/sysconfig/clock", "etc/conf.d/clock"):
194 tzpath = os.path.join(_root, filename)
195 if not os.path.isfile(tzpath):
196 continue
197
198 with open(tzpath) as tzfile:
199 data = tzfile.readlines()
200
201 for line in data:
202 # Look for the ZONE= setting.
203 match = zone_re.match(line)
204 if match is None:
205 # No ZONE= setting. Look for the TIMEZONE= setting.
206 match = timezone_re.match(line)
207
208 if match is not None:
209 # Some setting existed
210 line = line[match.end() :]
211 etctz = line[
212 : cast(
213 "re.Match[str]",
214 end_re.search(line),
215 ).start()
216 ]
217
218 parts = list(reversed(etctz.replace(" ", "_").split(os.path.sep)))
219 tzpath_parts: list[str] = []
220 while parts:
221 tzpath_parts.insert(0, parts.pop(0))
222
223 with contextlib.suppress(InvalidTimezone):
224 return Timezone(os.path.join(*tzpath_parts))
225
226 # systemd distributions use symlinks that include the zone name,
227 # see manpage of localtime(5) and timedatectl(1)
228 tzpath = os.path.join(_root, "etc", "localtime")
229 if os.path.isfile(tzpath) and os.path.islink(tzpath):
230 parts = list(
231 reversed(os.path.realpath(tzpath).replace(" ", "_").split(os.path.sep))
232 )
233 tzpath_parts: list[str] = [] # type: ignore[no-redef]
234 while parts:
235 tzpath_parts.insert(0, parts.pop(0))
236 with contextlib.suppress(InvalidTimezone):
237 return Timezone(os.path.join(*tzpath_parts))
238
239 # No explicit setting existed. Use localtime
240 for filename in ("etc/localtime", "usr/local/etc/localtime"):
241 tzpath = os.path.join(_root, filename)
242
243 if not os.path.isfile(tzpath):
244 continue
245
246 with open(tzpath, "rb") as f:
247 return Timezone.from_file(f)
248
249 warnings.warn(
250 "Unable not find any timezone configuration, defaulting to UTC.", stacklevel=1
251 )
252
253 return UTC
254
255
256def _tz_from_env(tzenv: str) -> Timezone:
257 if tzenv[0] == ":":
258 tzenv = tzenv[1:]
259
260 # TZ specifies a file
261 if os.path.isfile(tzenv):
262 with open(tzenv, "rb") as f:
263 return Timezone.from_file(f)
264
265 # TZ specifies a zoneinfo zone.
266 try:
267 return Timezone(tzenv)
268 except ValueError:
269 raise