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) -> None: 

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) -> None: 

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(self, dt: datetime, tz: datetime.tzinfo | str | None) -> datetime: ... 

78 

79 @overload 

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

81 

82 def localize( 

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

84 ) -> datetime | datetime.time: 

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

86 

87 Returns: 

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

89 :class:`datetime.datetime` is given. 

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

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

92 """ 

93 if isinstance(tz, str): 

94 tz = self.timezone(tz) 

95 if tz is None: 

96 return dt.replace(tzinfo=None) 

97 if isinstance(dt, time): 

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

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

100 return localized.timetz() 

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

102 

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

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

105 if it is not already known. 

106 

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

108 custom timezone is returned from timezone(). 

109 """ 

110 _unclean_id = timezone_component["TZID"] 

111 _id = self.clean_timezone_id(_unclean_id) 

112 if ( 

113 not self.__provider.knows_timezone_id(_id) 

114 and not self.__provider.knows_timezone_id(_unclean_id) 

115 and _id not in self.__tz_cache 

116 ): 

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

118 

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

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

121 self.__provider.fix_rrule_until(rrule, ical_rrule) 

122 

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

124 """Create a timezone from a timezone component. 

125 

126 This component will not be cached. 

127 """ 

128 return self.__provider.create_timezone(timezone_component) 

129 

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

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

132 

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

134 Internally, we should use this to identify timezones. 

135 """ 

136 return tzid.strip("/") 

137 

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

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

140 

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

142 timezone name, or a "globally unique" identifier 

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

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

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

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

147 primary ID so the next lookup is fast. 

148 """ 

149 primary = None 

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

151 if primary is None: 

152 primary = lookup_id 

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

154 if tz is not None: 

155 if is_global_guess: 

156 from icalendar.error import GloballyUniqueTZIDGuessed 

157 

158 warnings.warn( 

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

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

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

162 GloballyUniqueTZIDGuessed, 

163 stacklevel=3, 

164 ) 

165 self.__tz_cache[primary] = tz 

166 return tz 

167 return None 

168 

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

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

171 

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

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

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

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

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

177 libical, Evolution and Mozilla Lightning—the trailing Olson 

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

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

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

181 4. The original, unmodified ID. 

182 """ 

183 cleaned = self.clean_timezone_id(tz_id) 

184 yield cleaned, False 

185 if cleaned in WINDOWS_TO_OLSON: 

186 yield WINDOWS_TO_OLSON[cleaned], False 

187 if tz_id.startswith("/"): 

188 parts = cleaned.split("/") 

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

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

191 yield tz_id, False 

192 

193 def uses_pytz(self) -> bool: 

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

195 return self.__provider.uses_pytz() 

196 

197 def uses_zoneinfo(self) -> bool: 

198 """Whether we use zoneinfo.""" 

199 return self.__provider.uses_zoneinfo() 

200 

201 @property 

202 def name(self) -> str: 

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

204 return self.__provider.name 

205 

206 def __repr__(self) -> str: 

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

208 

209 

210__all__ = ["TZP"]