Source code for icalendar.timezone.tzp

from __future__ import annotations

import warnings
from datetime import datetime, time
from typing import TYPE_CHECKING, overload

from icalendar.tools import to_datetime

from .windows_to_olson import WINDOWS_TO_OLSON

if TYPE_CHECKING:
    from collections.abc import Iterator

    from dateutil.rrule import rrule

    from icalendar import prop
    from icalendar.cal import Timezone

    from .provider import TZProvider

DEFAULT_TIMEZONE_PROVIDER = "zoneinfo"


[docs] class TZP: """This is the timezone provider proxy. If you would like to have another timezone implementation, you can create a new one and pass it to this proxy. All of icalendar will then use this timezone implementation. """ def __init__(self, provider: str | TZProvider = DEFAULT_TIMEZONE_PROVIDER) -> None: """Create a new timezone implementation proxy.""" self.use(provider)
[docs] def use_pytz(self) -> None: """Use pytz as the timezone provider.""" from .pytz import PYTZ # noqa: PLC0415, RUF100 self._use(PYTZ())
[docs] def use_zoneinfo(self) -> None: """Use zoneinfo as the timezone provider.""" from .zoneinfo import ZONEINFO # noqa: PLC0415, RUF100 self._use(ZONEINFO())
def _use(self, provider: TZProvider) -> None: """Use a timezone implementation.""" self.__tz_cache = {} self.__provider = provider
[docs] def use(self, provider: str | TZProvider): """Switch to a different timezone provider.""" if isinstance(provider, str): use_provider = getattr(self, f"use_{provider}", None) if use_provider is None: raise ValueError( f"Unknown provider {provider}. Use 'pytz' or 'zoneinfo'." ) use_provider() else: self._use(provider)
[docs] def use_default(self) -> None: """Use the default timezone provider.""" self.use(DEFAULT_TIMEZONE_PROVIDER)
[docs] def localize_utc(self, dt: datetime.date) -> datetime.datetime: """Return the datetime in UTC. If the datetime has no timezone, set UTC as its timezone. """ return self.__provider.localize_utc(to_datetime(dt))
@overload def localize(self, dt: datetime, tz: datetime.tzinfo | str | None) -> datetime: ... @overload def localize(self, dt: time, tz: datetime.tzinfo | str | None) -> time: ...
[docs] def localize( self, dt: datetime.date | time, tz: datetime.tzinfo | str | None ) -> datetime | datetime.time: """Localize a datetime or time to a timezone. Returns: - A localized :class:`datetime.datetime` when a :class:`datetime.datetime` is given. - A localized :class:`datetime.time` when a :class:`datetime.time` is given. """ if isinstance(tz, str): tz = self.timezone(tz) if tz is None: return dt.replace(tzinfo=None) if isinstance(dt, time): dt_full = datetime.combine(datetime(2020, 1, 1), dt) # noqa: DTZ001 localized = self.__provider.localize(dt_full, tz) return localized.timetz() return self.__provider.localize(to_datetime(dt), tz)
[docs] def cache_timezone_component(self, timezone_component: Timezone.Timezone) -> None: """Cache the timezone that is created from a timezone component if it is not already known. This can influence the result from timezone(): Once cached, the custom timezone is returned from timezone(). """ _unclean_id = timezone_component["TZID"] _id = self.clean_timezone_id(_unclean_id) if ( not self.__provider.knows_timezone_id(_id) and not self.__provider.knows_timezone_id(_unclean_id) and _id not in self.__tz_cache ): self.__tz_cache[_id] = timezone_component.to_tz(self, lookup_tzid=False)
[docs] def fix_rrule_until(self, rrule: rrule, ical_rrule: prop.vRecur) -> None: """Make sure the until value works.""" self.__provider.fix_rrule_until(rrule, ical_rrule)
[docs] def create_timezone(self, timezone_component: Timezone.Timezone) -> datetime.tzinfo: """Create a timezone from a timezone component. This component will not be cached. """ return self.__provider.create_timezone(timezone_component)
[docs] def clean_timezone_id(self, tzid: str) -> str: """Return a clean version of the timezone id. Timezone ids can be a bit unclean, starting with a / for example. Internally, we should use this to identify timezones. """ return tzid.strip("/")
[docs] def timezone(self, tz_id: str) -> datetime.tzinfo | None: """Return a timezone with an ID or ``None`` if we can't find it. ``tz_id`` may be a plain Olson name (``Europe/Berlin``), a Windows timezone name, or a "globally unique" identifier (:rfc:`5545#section-3.2.19`) such as ``/freeassociation.sourceforge.net/Europe/Berlin``. We try the candidate IDs from ``_lookup_ids`` in order, checking the cache before the provider for each one, and cache the first match under the primary ID so the next lookup is fast. """ primary = None for lookup_id, is_global_guess in self._lookup_ids(tz_id): if primary is None: primary = lookup_id tz = self.__tz_cache.get(lookup_id) or self.__provider.timezone(lookup_id) if tz is not None: if is_global_guess: from icalendar.error import GloballyUniqueTZIDGuessed warnings.warn( f"Timezone {tz_id!r} is a globally unique TZID; " f"guessing it means {lookup_id!r} by stripping the vendor " "prefix. This may be wrong. See RFC 5545 section 3.2.19.", GloballyUniqueTZIDGuessed, stacklevel=3, ) self.__tz_cache[primary] = tz return tz return None
def _lookup_ids(self, tz_id: str) -> Iterator[tuple[str, bool]]: """Yield ``(id, is_global_guess)`` tuples to try, best match first. 1. The cleaned ID, without any surrounding ``/``. 2. The Olson name of a Windows timezone (for example, ``W. Europe Standard Time`` -> ``Europe/Berlin``). 3. For a "globally unique" TZID (:rfc:`5545#section-3.2.19`) of the form ``/<vendor>/<Olson/Name>``—emitted by clients such as libical, Evolution and Mozilla Lightning—the trailing Olson identifier, dropping vendor path components from the front. The longest suffix is tried first, so multi-part names such as ``America/Argentina/Buenos_Aires`` still match. 4. The original, unmodified ID. """ cleaned = self.clean_timezone_id(tz_id) yield cleaned, False if cleaned in WINDOWS_TO_OLSON: yield WINDOWS_TO_OLSON[cleaned], False if tz_id.startswith("/"): parts = cleaned.split("/") for start in range(1, len(parts)): yield "/".join(parts[start:]), True yield tz_id, False
[docs] def uses_pytz(self) -> bool: """Whether we use pytz at all.""" return self.__provider.uses_pytz()
[docs] def uses_zoneinfo(self) -> bool: """Whether we use zoneinfo.""" return self.__provider.uses_zoneinfo()
@property def name(self) -> str: """The name of the timezone component used.""" return self.__provider.name def __repr__(self) -> str: return f"{self.__class__.__name__}({self.name!r})"
__all__ = ["TZP"]