Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/icalendar/timezone/tzp.py: 84%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

111 statements  

1from __future__ import annotations 

2 

3import warnings 

4from datetime import date, datetime, time, tzinfo 

5from typing import TYPE_CHECKING, overload 

6 

7from icalendar.tools import to_datetime 

8 

9from .windows_to_olson import WINDOWS_TO_OLSON 

10 

11if TYPE_CHECKING: 

12 from collections.abc import Iterator 

13 

14 from dateutil.rrule import rrule 

15 

16 from icalendar import prop 

17 from icalendar.cal import Timezone 

18 

19 from .provider import TZProvider 

20 

21DEFAULT_TIMEZONE_PROVIDER = "zoneinfo" 

22 

23#: Reference date used to localize a bare :class:`datetime.time`. 

24_TIME_REFERENCE_DATE = date(2020, 1, 1) 

25 

26 

27class TZP: 

28 """This is the timezone provider proxy. 

29 

30 If you would like to have another timezone implementation, 

31 you can create a new one and pass it to this proxy. 

32 All of icalendar will then use this timezone implementation. 

33 """ 

34 

35 def __init__(self, provider: str | TZProvider = DEFAULT_TIMEZONE_PROVIDER) -> None: 

36 """Create a new timezone implementation proxy.""" 

37 self.use(provider) 

38 

39 def use_pytz(self) -> None: 

40 """Use pytz as the timezone provider.""" 

41 from .pytz import PYTZ # noqa: PLC0415, RUF100 

42 

43 self._use(PYTZ()) 

44 

45 def use_zoneinfo(self) -> None: 

46 """Use zoneinfo as the timezone provider.""" 

47 from .zoneinfo import ZONEINFO # noqa: PLC0415, RUF100 

48 

49 self._use(ZONEINFO()) 

50 

51 def _use(self, provider: TZProvider) -> None: 

52 """Use a timezone implementation.""" 

53 self.__tz_cache = {} 

54 self.__guessed_ids = {} 

55 self.__provider = provider 

56 

57 def use(self, provider: str | TZProvider) -> None: 

58 """Switch to a different timezone provider.""" 

59 if isinstance(provider, str): 

60 use_provider = getattr(self, f"use_{provider}", None) 

61 if use_provider is None: 

62 raise ValueError( 

63 f"Unknown provider {provider}. Use 'pytz' or 'zoneinfo'." 

64 ) 

65 use_provider() 

66 else: 

67 self._use(provider) 

68 

69 def use_default(self) -> None: 

70 """Use the default timezone provider.""" 

71 self.use(DEFAULT_TIMEZONE_PROVIDER) 

72 

73 @overload 

74 def localize_utc(self, dt: datetime) -> datetime: ... 

75 

76 @overload 

77 def localize_utc(self, dt: time) -> time: ... 

78 

79 @overload 

80 def localize_utc(self, dt: date) -> datetime: ... 

81 

82 def localize_utc(self, dt: date | datetime | time) -> datetime | time: 

83 """Return the value in UTC. 

84 

85 If the value has no timezone, set UTC as its timezone. 

86 Otherwise, the same date/time in UTC is returned. 

87 

88 Returns: 

89 - A localized :class:`datetime.datetime` in UTC when a 

90 :class:`datetime.datetime` is given. 

91 - A localized :class:`datetime.datetime` at midnight in UTC 

92 when a :class:`datetime.date` is given. 

93 - A localized :class:`datetime.time` in UTC when a 

94 :class:`datetime.time` is given. 

95 """ 

96 if isinstance(dt, time): 

97 dt_full = datetime.combine(_TIME_REFERENCE_DATE, dt) 

98 return self.__provider.localize_utc(dt_full).timetz() 

99 return self.__provider.localize_utc(to_datetime(dt)) 

100 

101 @overload 

102 def localize(self, dt: datetime, tz: tzinfo | str | None) -> datetime: ... 

103 

104 @overload 

105 def localize(self, dt: time, tz: tzinfo | str | None) -> time: ... 

106 

107 @overload 

108 def localize(self, dt: date, tz: tzinfo | str | None) -> datetime: ... 

109 

110 def localize( 

111 self, dt: date | datetime | time, tz: tzinfo | str | None 

112 ) -> datetime | time: 

113 """Localize a date, datetime or time to a timezone. 

114 

115 Use ``tz=None`` to remove the timezone. 

116 

117 Returns: 

118 - A localized :class:`datetime.datetime` when a 

119 :class:`datetime.datetime` is given. 

120 - A localized :class:`datetime.datetime` at midnight when a 

121 :class:`datetime.date` is given. 

122 - A localized :class:`datetime.time` when a 

123 :class:`datetime.time` is given. 

124 """ 

125 if isinstance(tz, str): 

126 tz = self.timezone(tz) 

127 if isinstance(dt, time): 

128 if tz is None: 

129 return dt.replace(tzinfo=None) 

130 dt_full = datetime.combine(_TIME_REFERENCE_DATE, dt) 

131 return self.__provider.localize(dt_full, tz).timetz() 

132 dt = to_datetime(dt) 

133 if tz is None: 

134 return dt.replace(tzinfo=None) 

135 return self.__provider.localize(dt, tz) 

136 

137 def cache_timezone_component(self, timezone_component: Timezone) -> None: 

138 """Cache the timezone that is created from a timezone component 

139 if it is not already known. 

140 

141 This can influence the result from timezone(): Once cached, the 

142 custom timezone is returned from timezone(). 

143 """ 

144 _unclean_id = timezone_component["TZID"] 

145 _id = self.clean_timezone_id(_unclean_id) 

146 if ( 

147 not self.__provider.knows_timezone_id(_id) 

148 and not self.__provider.knows_timezone_id(_unclean_id) 

149 and _id not in self.__tz_cache 

150 ): 

151 self.__tz_cache[_id] = timezone_component.to_tz(self, lookup_tzid=False) 

152 

153 def fix_rrule_until(self, rrule: rrule, ical_rrule: prop.vRecur) -> None: 

154 """Make sure the until value works.""" 

155 self.__provider.fix_rrule_until(rrule, ical_rrule) 

156 

157 def create_timezone(self, timezone_component: Timezone) -> tzinfo: 

158 """Create a timezone from a timezone component. 

159 

160 This component will not be cached. 

161 """ 

162 return self.__provider.create_timezone(timezone_component) 

163 

164 def clean_timezone_id(self, tzid: str) -> str: 

165 """Return a clean version of the timezone id. 

166 

167 Timezone ids can be a bit unclean, starting with a / for example. 

168 Internally, we should use this to identify timezones. 

169 """ 

170 return tzid.strip("/") 

171 

172 def timezone(self, tz_id: str) -> tzinfo | None: 

173 """Return a timezone with an ID or ``None`` if we can't find it. 

174 

175 ``tz_id`` may be a plain Olson name (``Europe/Berlin``), a Windows 

176 timezone name, or a "globally unique" identifier 

177 (:rfc:`5545#section-3.2.19`) such as 

178 ``/freeassociation.sourceforge.net/Europe/Berlin``. We try the 

179 candidate IDs from ``_lookup_ids`` in order, checking the cache 

180 before the provider for each one, and cache the first match under the 

181 primary ID so the next lookup is fast. 

182 

183 A lookup that is resolved by guessing the trailing Olson identifier 

184 emits :class:`~icalendar.error.GloballyUniqueTZIDGuessed` on every 

185 call, including when the result is served from the cache. 

186 """ 

187 primary = None 

188 for lookup_id, is_global_guess in self._lookup_ids(tz_id): 

189 if primary is None: 

190 primary = lookup_id 

191 tz = self.__tz_cache.get(lookup_id) or self.__provider.timezone(lookup_id) 

192 if tz is not None: 

193 # Warn on every lookup that relies on a guess, including 

194 # cache hits: the cache key is the cleaned id, so remember 

195 # which ids were resolved by guessing. 

196 guessed_id = ( 

197 lookup_id if is_global_guess else self.__guessed_ids.get(lookup_id) 

198 ) 

199 if guessed_id is not None: 

200 from icalendar.error import GloballyUniqueTZIDGuessed 

201 

202 warnings.warn( 

203 f"Timezone {tz_id!r} is a globally unique TZID; " 

204 f"guessing it means {guessed_id!r} by stripping the vendor " 

205 "prefix. This may be wrong. See RFC 5545 section 3.2.19.", 

206 GloballyUniqueTZIDGuessed, 

207 stacklevel=3, 

208 ) 

209 self.__guessed_ids[primary] = guessed_id 

210 self.__tz_cache[primary] = tz 

211 return tz 

212 return None 

213 

214 def _lookup_ids(self, tz_id: str) -> Iterator[tuple[str, bool]]: 

215 """Yield ``(id, is_global_guess)`` tuples to try, best match first. 

216 

217 1. The cleaned ID, without any surrounding ``/``. 

218 2. The Olson name of a Windows timezone (for example, 

219 ``W. Europe Standard Time`` -> ``Europe/Berlin``). 

220 3. For a "globally unique" TZID (:rfc:`5545#section-3.2.19`) of the 

221 form ``/<vendor>/<Olson/Name>``—emitted by clients such as 

222 libical, Evolution and Mozilla Lightning—the trailing Olson 

223 identifier, dropping vendor path components from the front. The 

224 longest suffix is tried first, so multi-part names such as 

225 ``America/Argentina/Buenos_Aires`` still match. 

226 4. The original, unmodified ID. 

227 """ 

228 cleaned = self.clean_timezone_id(tz_id) 

229 yield cleaned, False 

230 if cleaned in WINDOWS_TO_OLSON: 

231 yield WINDOWS_TO_OLSON[cleaned], False 

232 if tz_id.startswith("/"): 

233 parts = cleaned.split("/") 

234 for start in range(1, len(parts)): 

235 yield "/".join(parts[start:]), True 

236 yield tz_id, False 

237 

238 def uses_pytz(self) -> bool: 

239 """Whether we use pytz at all.""" 

240 return self.__provider.uses_pytz() 

241 

242 def uses_zoneinfo(self) -> bool: 

243 """Whether we use zoneinfo.""" 

244 return self.__provider.uses_zoneinfo() 

245 

246 @property 

247 def name(self) -> str: 

248 """The name of the timezone component used.""" 

249 return self.__provider.name 

250 

251 def __repr__(self) -> str: 

252 return f"{self.__class__.__name__}({self.name!r})" 

253 

254 

255__all__ = ["TZP"]