timezone.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. # mypy: no-warn-redundant-casts
  2. from __future__ import annotations
  3. import datetime as _datetime
  4. from abc import ABC
  5. from abc import abstractmethod
  6. from typing import TYPE_CHECKING
  7. from typing import TypeVar
  8. from typing import cast
  9. from pendulum.tz.exceptions import AmbiguousTime
  10. from pendulum.tz.exceptions import InvalidTimezone
  11. from pendulum.tz.exceptions import NonExistingTime
  12. from pendulum.utils._compat import zoneinfo
  13. if TYPE_CHECKING:
  14. from typing_extensions import Self
  15. POST_TRANSITION = "post"
  16. PRE_TRANSITION = "pre"
  17. TRANSITION_ERROR = "error"
  18. _DT = TypeVar("_DT", bound=_datetime.datetime)
  19. class PendulumTimezone(ABC):
  20. @property
  21. @abstractmethod
  22. def name(self) -> str:
  23. raise NotImplementedError
  24. @abstractmethod
  25. def convert(self, dt: _DT, raise_on_unknown_times: bool = False) -> _DT:
  26. raise NotImplementedError
  27. @abstractmethod
  28. def datetime(
  29. self,
  30. year: int,
  31. month: int,
  32. day: int,
  33. hour: int = 0,
  34. minute: int = 0,
  35. second: int = 0,
  36. microsecond: int = 0,
  37. ) -> _datetime.datetime:
  38. raise NotImplementedError
  39. class Timezone(zoneinfo.ZoneInfo, PendulumTimezone):
  40. """
  41. Represents a named timezone.
  42. The accepted names are those provided by the IANA time zone database.
  43. >>> from pendulum.tz.timezone import Timezone
  44. >>> tz = Timezone('Europe/Paris')
  45. """
  46. def __new__(cls, key: str) -> Self:
  47. try:
  48. return super().__new__(cls, key) # type: ignore[call-arg]
  49. except zoneinfo.ZoneInfoNotFoundError:
  50. raise InvalidTimezone(key)
  51. @property
  52. def name(self) -> str:
  53. return self.key
  54. def convert(self, dt: _DT, raise_on_unknown_times: bool = False) -> _DT:
  55. """
  56. Converts a datetime in the current timezone.
  57. If the datetime is naive, it will be normalized.
  58. >>> from datetime import datetime
  59. >>> from pendulum import timezone
  60. >>> paris = timezone('Europe/Paris')
  61. >>> dt = datetime(2013, 3, 31, 2, 30, fold=1)
  62. >>> in_paris = paris.convert(dt)
  63. >>> in_paris.isoformat()
  64. '2013-03-31T03:30:00+02:00'
  65. If the datetime is aware, it will be properly converted.
  66. >>> new_york = timezone('America/New_York')
  67. >>> in_new_york = new_york.convert(in_paris)
  68. >>> in_new_york.isoformat()
  69. '2013-03-30T21:30:00-04:00'
  70. """
  71. if dt.tzinfo is None:
  72. # Technically, utcoffset() can return None, but none of the zone information
  73. # in tzdata sets _tti_before to None. This can be checked with the following
  74. # code:
  75. #
  76. # >>> import zoneinfo
  77. # >>> from zoneinfo._zoneinfo import ZoneInfo
  78. #
  79. # >>> for tzname in zoneinfo.available_timezones():
  80. # >>> if ZoneInfo(tzname)._tti_before is None:
  81. # >>> print(tzname)
  82. offset_before = cast(
  83. _datetime.timedelta,
  84. (self.utcoffset(dt.replace(fold=0)) if dt.fold else self.utcoffset(dt)),
  85. )
  86. offset_after = cast(
  87. _datetime.timedelta,
  88. (self.utcoffset(dt) if dt.fold else self.utcoffset(dt.replace(fold=1))),
  89. )
  90. if offset_after > offset_before:
  91. # Skipped time
  92. if raise_on_unknown_times:
  93. raise NonExistingTime(dt)
  94. dt = cast(
  95. _DT,
  96. dt
  97. + (
  98. (offset_after - offset_before)
  99. if dt.fold
  100. else (offset_before - offset_after)
  101. ),
  102. )
  103. elif offset_before > offset_after and raise_on_unknown_times:
  104. # Repeated time
  105. raise AmbiguousTime(dt)
  106. return dt.replace(tzinfo=self)
  107. return cast(_DT, dt.astimezone(self))
  108. def datetime(
  109. self,
  110. year: int,
  111. month: int,
  112. day: int,
  113. hour: int = 0,
  114. minute: int = 0,
  115. second: int = 0,
  116. microsecond: int = 0,
  117. ) -> _datetime.datetime:
  118. """
  119. Return a normalized datetime for the current timezone.
  120. """
  121. return self.convert(
  122. _datetime.datetime(
  123. year, month, day, hour, minute, second, microsecond, fold=1
  124. )
  125. )
  126. def __repr__(self) -> str:
  127. return f"{self.__class__.__name__}('{self.name}')"
  128. class FixedTimezone(_datetime.tzinfo, PendulumTimezone):
  129. def __init__(self, offset: int, name: str | None = None) -> None:
  130. sign = "-" if offset < 0 else "+"
  131. minutes = offset / 60
  132. hour, minute = divmod(abs(int(minutes)), 60)
  133. if not name:
  134. name = f"{sign}{hour:02d}:{minute:02d}"
  135. self._name = name
  136. self._offset = offset
  137. self._utcoffset = _datetime.timedelta(seconds=offset)
  138. @property
  139. def name(self) -> str:
  140. return self._name
  141. def convert(self, dt: _DT, raise_on_unknown_times: bool = False) -> _DT:
  142. if dt.tzinfo is None:
  143. return dt.__class__(
  144. dt.year,
  145. dt.month,
  146. dt.day,
  147. dt.hour,
  148. dt.minute,
  149. dt.second,
  150. dt.microsecond,
  151. tzinfo=self,
  152. fold=0,
  153. )
  154. return cast(_DT, dt.astimezone(self))
  155. def datetime(
  156. self,
  157. year: int,
  158. month: int,
  159. day: int,
  160. hour: int = 0,
  161. minute: int = 0,
  162. second: int = 0,
  163. microsecond: int = 0,
  164. ) -> _datetime.datetime:
  165. return self.convert(
  166. _datetime.datetime(
  167. year, month, day, hour, minute, second, microsecond, fold=1
  168. )
  169. )
  170. @property
  171. def offset(self) -> int:
  172. return self._offset
  173. def utcoffset(self, dt: _datetime.datetime | None) -> _datetime.timedelta:
  174. return self._utcoffset
  175. def dst(self, dt: _datetime.datetime | None) -> _datetime.timedelta:
  176. return _datetime.timedelta()
  177. def fromutc(self, dt: _datetime.datetime) -> _datetime.datetime:
  178. # Use the stdlib datetime's add method to avoid infinite recursion
  179. return (_datetime.datetime.__add__(dt, self._utcoffset)).replace(tzinfo=self)
  180. def tzname(self, dt: _datetime.datetime | None) -> str | None:
  181. return self._name
  182. def __getinitargs__(self) -> tuple[int, str]:
  183. return self._offset, self._name
  184. def __repr__(self) -> str:
  185. name = ""
  186. if self._name:
  187. name = f', name="{self._name}"'
  188. return f"{self.__class__.__name__}({self._offset}{name})"
  189. UTC = Timezone("UTC")