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