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

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

94 statements  

1from __future__ import annotations 

2 

3import warnings 

4from datetime import datetime, time 

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 

24class TZP: 

25 """This is the timezone provider proxy. 

26 

27 If you would like to have another timezone implementation, 

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

29 All of icalendar will then use this timezone implementation. 

30 """ 

31 

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

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

34 self.use(provider) 

35 

36 def use_pytz(self) -> None: 

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

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

39 

40 self._use(PYTZ()) 

41 

42 def use_zoneinfo(self) -> None: 

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

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

45 

46 self._use(ZONEINFO()) 

47 

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

49 """Use a timezone implementation.""" 

50 self.__tz_cache = {} 

51 self.__provider = provider 

52 

53 def use(self, provider: str | TZProvider): 

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

55 if isinstance(provider, str): 

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

57 if use_provider is None: 

58 raise ValueError( 

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

60 ) 

61 use_provider() 

62 else: 

63 self._use(provider) 

64 

65 def use_default(self): 

66 """Use the default timezone provider.""" 

67 self.use(DEFAULT_TIMEZONE_PROVIDER) 

68 

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

70 """Return the datetime in UTC. 

71 

72 If the datetime has no timezone, set UTC as its timezone. 

73 """ 

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

75 

76 @overload 

77 def localize( 

78 self, dt: datetime.datetime, tz: datetime.tzinfo | str | None 

79 ) -> datetime.datetime: ... 

80 

81 @overload 

82 def localize( 

83 self, dt: datetime.time, tz: datetime.tzinfo | str | None 

84 ) -> datetime.time: ... 

85 

86 def localize( 

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

88 ) -> datetime.datetime | datetime.time: 

89 """Localize a datetime or time to a timezone. 

90 

91 Returns: 

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

93 :class:`datetime.datetime` is given. 

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

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

96 """ 

97 if isinstance(tz, str): 

98 tz = self.timezone(tz) 

99 if tz is None: 

100 return dt.replace(tzinfo=None) 

101 if isinstance(dt, time): 

102 dt_full = datetime.combine(datetime(2020, 1, 1), dt) # noqa: DTZ001 

103 localized = self.__provider.localize(dt_full, tz) 

104 return localized.timetz() 

105 return self.__provider.localize(to_datetime(dt), tz) 

106 

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

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

109 if it is not already known. 

110 

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

112 custom timezone is returned from timezone(). 

113 """ 

114 _unclean_id = timezone_component["TZID"] 

115 _id = self.clean_timezone_id(_unclean_id) 

116 if ( 

117 not self.__provider.knows_timezone_id(_id) 

118 and not self.__provider.knows_timezone_id(_unclean_id) 

119 and _id not in self.__tz_cache 

120 ): 

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

122 

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

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

125 self.__provider.fix_rrule_until(rrule, ical_rrule) 

126 

127 def create_timezone(self, timezone_component: Timezone.Timezone) -> datetime.tzinfo: 

128 """Create a timezone from a timezone component. 

129 

130 This component will not be cached. 

131 """ 

132 return self.__provider.create_timezone(timezone_component) 

133 

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

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

136 

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

138 Internally, we should use this to identify timezones. 

139 """ 

140 return tzid.strip("/") 

141 

142 def timezone(self, tz_id: str) -> datetime.tzinfo | None: 

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

144 

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

146 timezone name, or a "globally unique" identifier 

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

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

149 candidate IDs from :meth:`_lookup_ids` in order, checking the cache 

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

151 primary ID so the next lookup is fast. 

152 """ 

153 primary = None 

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

155 if primary is None: 

156 primary = lookup_id 

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

158 if tz is not None: 

159 if is_global_guess: 

160 from icalendar.error import GloballyUniqueTZIDGuessed 

161 

162 warnings.warn( 

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

164 f"guessing it means {lookup_id!r} by stripping the vendor " 

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

166 GloballyUniqueTZIDGuessed, 

167 stacklevel=3, 

168 ) 

169 self.__tz_cache[primary] = tz 

170 return tz 

171 return None 

172 

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

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

175 

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

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

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

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

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

181 libical, Evolution and Mozilla Lightning—the trailing Olson 

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

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

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

185 4. The original, unmodified ID. 

186 """ 

187 cleaned = self.clean_timezone_id(tz_id) 

188 yield cleaned, False 

189 if cleaned in WINDOWS_TO_OLSON: 

190 yield WINDOWS_TO_OLSON[cleaned], False 

191 if tz_id.startswith("/"): 

192 parts = cleaned.split("/") 

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

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

195 yield tz_id, False 

196 

197 def uses_pytz(self) -> bool: 

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

199 return self.__provider.uses_pytz() 

200 

201 def uses_zoneinfo(self) -> bool: 

202 """Whether we use zoneinfo.""" 

203 return self.__provider.uses_zoneinfo() 

204 

205 @property 

206 def name(self) -> str: 

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

208 return self.__provider.name 

209 

210 def __repr__(self) -> str: 

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

212 

213 

214__all__ = ["TZP"]