88
99from pendulum .utils ._compat import zoneinfo
1010
11+ from .exceptions import AmbiguousTime
12+ from .exceptions import InvalidTimezone
13+ from .exceptions import NonExistingTime
14+
1115
1216POST_TRANSITION = "post"
1317PRE_TRANSITION = "pre"
@@ -51,11 +55,17 @@ class Timezone(zoneinfo.ZoneInfo, PendulumTimezone):
5155 >>> tz = Timezone('Europe/Paris')
5256 """
5357
58+ def __new__ (cls , key : str ) -> "Timezone" :
59+ try :
60+ return super ().__new__ (cls , key )
61+ except zoneinfo .ZoneInfoNotFoundError :
62+ raise InvalidTimezone (key )
63+
5464 @property
5565 def name (self ) -> str :
5666 return self .key
5767
58- def convert (self , dt : datetime , dst_rule : Optional [ str ] = None ) -> datetime :
68+ def convert (self , dt : datetime , raise_on_unknown_times : bool = False ) -> datetime :
5969 """
6070 Converts a datetime in the current timezone.
6171
@@ -76,14 +86,30 @@ def convert(self, dt: datetime, dst_rule: Optional[str] = None) -> datetime:
7686 >>> in_new_york.isoformat()
7787 '2013-03-30T21:30:00-04:00'
7888 """
79- if dst_rule is not None :
80- if dst_rule == PRE_TRANSITION and dt .fold != 0 :
81- dt = dt .replace (fold = 0 )
82- elif dst_rule == POST_TRANSITION and dt .fold != 1 :
83- dt = dt .replace (fold = 1 )
84-
8589 if dt .tzinfo is None :
86- dt = dt .replace (tzinfo = self )
90+ offset_before = (
91+ self .utcoffset (dt .replace (fold = 0 )) if dt .fold else self .utcoffset (dt )
92+ )
93+ offset_after = (
94+ self .utcoffset (dt ) if dt .fold else self .utcoffset (dt .replace (fold = 1 ))
95+ )
96+
97+ if offset_after > offset_before :
98+ # Skipped time
99+ if raise_on_unknown_times :
100+ raise NonExistingTime (dt )
101+
102+ dt += (
103+ (offset_after - offset_before )
104+ if dt .fold
105+ else (offset_before - offset_after )
106+ )
107+ elif offset_before > offset_after :
108+ # Repeated time
109+ if raise_on_unknown_times :
110+ raise AmbiguousTime (dt )
111+
112+ return dt .replace (tzinfo = self )
87113
88114 return dt .astimezone (self )
89115
@@ -100,10 +126,13 @@ def datetime(
100126 """
101127 Return a normalized datetime for the current timezone.
102128 """
103- return datetime (
104- year , month , day , hour , minute , second , microsecond , tzinfo = self , fold = 1
129+ return self . convert (
130+ datetime ( year , month , day , hour , minute , second , microsecond , fold = 1 )
105131 )
106132
133+ def __repr__ (self ) -> str :
134+ return f"{ self .__class__ .__name__ } ('{ self .name } ')"
135+
107136
108137class FixedTimezone (tzinfo , PendulumTimezone ):
109138 def __init__ (self , offset : int , name : Optional [str ] = None ) -> None :
@@ -123,7 +152,7 @@ def __init__(self, offset: int, name: Optional[str] = None) -> None:
123152 def name (self ) -> str :
124153 return self ._name
125154
126- def convert (self , dt : datetime , dst_rule : Optional [ str ] = None ) -> datetime :
155+ def convert (self , dt : datetime , raise_on_unknown_times : bool = False ) -> datetime :
127156 if dt .tzinfo is None :
128157 return dt .__class__ (
129158 dt .year ,
@@ -137,12 +166,6 @@ def convert(self, dt: datetime, dst_rule: Optional[str] = None) -> datetime:
137166 fold = 0 ,
138167 )
139168
140- if dst_rule is not None :
141- if dst_rule == PRE_TRANSITION and dt .fold != 0 :
142- dt = dt .replace (fold = 0 )
143- elif dst_rule == POST_TRANSITION and dt .fold != 1 :
144- dt = dt .replace (fold = 1 )
145-
146169 return dt .astimezone (self )
147170
148171 def datetime (
@@ -179,5 +202,12 @@ def tzname(self, dt: Optional[datetime]) -> Optional[str]:
179202 def __getinitargs__ (self ): # type: () -> tuple
180203 return self ._offset , self ._name
181204
205+ def __repr__ (self ) -> str :
206+ name = ""
207+ if self ._name :
208+ name = f', name="{ self ._name } "'
209+
210+ return f"{ self .__class__ .__name__ } ({ self ._offset } { name } )"
211+
182212
183- UTC = FixedTimezone ( 0 , "UTC" )
213+ UTC = Timezone ( "UTC" )
0 commit comments