1from __future__ import annotations
2
3from datetime import datetime, time
4from typing import TYPE_CHECKING, overload
5
6from icalendar.tools import to_datetime
7
8from .windows_to_olson import WINDOWS_TO_OLSON
9
10if TYPE_CHECKING:
11 from dateutil.rrule import rrule
12
13 from icalendar import prop
14 from icalendar.cal import Timezone
15
16 from .provider import TZProvider
17
18DEFAULT_TIMEZONE_PROVIDER = "zoneinfo"
19
20
21class TZP:
22 """This is the timezone provider proxy.
23
24 If you would like to have another timezone implementation,
25 you can create a new one and pass it to this proxy.
26 All of icalendar will then use this timezone implementation.
27 """
28
29 def __init__(self, provider: str | TZProvider = DEFAULT_TIMEZONE_PROVIDER):
30 """Create a new timezone implementation proxy."""
31 self.use(provider)
32
33 def use_pytz(self) -> None:
34 """Use pytz as the timezone provider."""
35 from .pytz import PYTZ # noqa: PLC0415, RUF100
36
37 self._use(PYTZ())
38
39 def use_zoneinfo(self) -> None:
40 """Use zoneinfo as the timezone provider."""
41 from .zoneinfo import ZONEINFO # noqa: PLC0415, RUF100
42
43 self._use(ZONEINFO())
44
45 def _use(self, provider: TZProvider) -> None:
46 """Use a timezone implementation."""
47 self.__tz_cache = {}
48 self.__provider = provider
49
50 def use(self, provider: str | TZProvider):
51 """Switch to a different timezone provider."""
52 if isinstance(provider, str):
53 use_provider = getattr(self, f"use_{provider}", None)
54 if use_provider is None:
55 raise ValueError(
56 f"Unknown provider {provider}. Use 'pytz' or 'zoneinfo'."
57 )
58 use_provider()
59 else:
60 self._use(provider)
61
62 def use_default(self):
63 """Use the default timezone provider."""
64 self.use(DEFAULT_TIMEZONE_PROVIDER)
65
66 def localize_utc(self, dt: datetime.date) -> datetime.datetime:
67 """Return the datetime in UTC.
68
69 If the datetime has no timezone, set UTC as its timezone.
70 """
71 return self.__provider.localize_utc(to_datetime(dt))
72
73 @overload
74 def localize(
75 self, dt: datetime.datetime, tz: datetime.tzinfo | str | None
76 ) -> datetime.datetime: ...
77
78 @overload
79 def localize(
80 self, dt: datetime.time, tz: datetime.tzinfo | str | None
81 ) -> datetime.time: ...
82
83 def localize(
84 self, dt: datetime.date | datetime.time, tz: datetime.tzinfo | str | None
85 ) -> datetime.datetime | datetime.time:
86 """Localize a datetime or time to a timezone.
87
88 Returns:
89 - A localized :class:`datetime.datetime` when a
90 :class:`datetime.datetime` is given.
91 - A localized :class:`datetime.time` when a
92 :class:`datetime.time` is given.
93 """
94 if isinstance(tz, str):
95 tz = self.timezone(tz)
96 if tz is None:
97 return dt.replace(tzinfo=None)
98 if isinstance(dt, time):
99 dt_full = datetime.combine(datetime(2020, 1, 1), dt) # noqa: DTZ001
100 localized = self.__provider.localize(dt_full, tz)
101 return localized.timetz()
102 return self.__provider.localize(to_datetime(dt), tz)
103
104 def cache_timezone_component(self, timezone_component: Timezone.Timezone) -> None:
105 """Cache the timezone that is created from a timezone component
106 if it is not already known.
107
108 This can influence the result from timezone(): Once cached, the
109 custom timezone is returned from timezone().
110 """
111 _unclean_id = timezone_component["TZID"]
112 _id = self.clean_timezone_id(_unclean_id)
113 if (
114 not self.__provider.knows_timezone_id(_id)
115 and not self.__provider.knows_timezone_id(_unclean_id)
116 and _id not in self.__tz_cache
117 ):
118 self.__tz_cache[_id] = timezone_component.to_tz(self, lookup_tzid=False)
119
120 def fix_rrule_until(self, rrule: rrule, ical_rrule: prop.vRecur) -> None:
121 """Make sure the until value works."""
122 self.__provider.fix_rrule_until(rrule, ical_rrule)
123
124 def create_timezone(self, timezone_component: Timezone.Timezone) -> datetime.tzinfo:
125 """Create a timezone from a timezone component.
126
127 This component will not be cached.
128 """
129 return self.__provider.create_timezone(timezone_component)
130
131 def clean_timezone_id(self, tzid: str) -> str:
132 """Return a clean version of the timezone id.
133
134 Timezone ids can be a bit unclean, starting with a / for example.
135 Internally, we should use this to identify timezones.
136 """
137 return tzid.strip("/")
138
139 def timezone(self, tz_id: str) -> datetime.tzinfo | None:
140 """Return a timezone with an id or None if we cannot find it."""
141 _unclean_id = tz_id
142 tz_id = self.clean_timezone_id(tz_id)
143 tz = self.__provider.timezone(tz_id)
144 if tz is not None:
145 return tz
146 if tz_id in WINDOWS_TO_OLSON:
147 tz = self.__provider.timezone(WINDOWS_TO_OLSON[tz_id])
148 return tz or self.__provider.timezone(_unclean_id) or self.__tz_cache.get(tz_id)
149
150 def uses_pytz(self) -> bool:
151 """Whether we use pytz at all."""
152 return self.__provider.uses_pytz()
153
154 def uses_zoneinfo(self) -> bool:
155 """Whether we use zoneinfo."""
156 return self.__provider.uses_zoneinfo()
157
158 @property
159 def name(self) -> str:
160 """The name of the timezone component used."""
161 return self.__provider.name
162
163 def __repr__(self) -> str:
164 return f"{self.__class__.__name__}({self.name!r})"
165
166
167__all__ = ["TZP"]