1919
2020import re
2121import warnings
22+ from typing import TYPE_CHECKING , SupportsInt
23+
24+ try :
25+ import pytz
26+ except ModuleNotFoundError :
27+ pytz = None
28+ import zoneinfo
29+
2230from bisect import bisect_right
2331from collections .abc import Iterable
2432from datetime import date , datetime , time , timedelta , tzinfo
25- from typing import TYPE_CHECKING , SupportsInt
26-
27- import pytz as _pytz
2833
34+ from babel import localtime
2935from babel .core import Locale , default_locale , get_global
3036from babel .localedata import LocaleDataDict
31- from babel .util import LOCALTZ , UTC
3237
3338if TYPE_CHECKING :
3439 from typing_extensions import Literal , TypeAlias
35-
3640 _Instant : TypeAlias = date | time | float | None
3741 _PredefinedTimeFormat : TypeAlias = Literal ['full' , 'long' , 'medium' , 'short' ]
3842 _Context : TypeAlias = Literal ['format' , 'stand-alone' ]
4852NO_INHERITANCE_MARKER = u'\u2205 \u2205 \u2205 '
4953
5054
55+ if pytz :
56+ UTC = pytz .utc
57+ else :
58+ UTC = zoneinfo .ZoneInfo ('UTC' )
59+ LOCALTZ = localtime .LOCALTZ
60+
5161LC_TIME = default_locale ('LC_TIME' )
5262
5363# Aliases for use in scopes where the modules are shadowed by local variables
5666time_ = time
5767
5868
69+ def _localize (tz : tzinfo , dt : datetime ) -> datetime :
70+ # Support localizing with both pytz and zoneinfo tzinfos
71+ # nothing to do
72+ if dt .tzinfo is tz :
73+ return dt
74+
75+ if hasattr (tz , 'localize' ): # pytz
76+ return tz .localize (dt )
77+
78+ if dt .tzinfo is None :
79+ # convert naive to localized
80+ return dt .replace (tzinfo = tz )
81+
82+ # convert timezones
83+ return dt .astimezone (tz )
84+
85+
86+
5987def _get_dt_and_tzinfo (dt_or_tzinfo : _DtOrTzinfo ) -> tuple [datetime_ | None , tzinfo ]:
6088 """
6189 Parse a `dt_or_tzinfo` value into a datetime and a tzinfo.
@@ -150,15 +178,15 @@ def _ensure_datetime_tzinfo(datetime: datetime_, tzinfo: tzinfo | None = None) -
150178
151179 If a tzinfo is passed in, the datetime is normalized to that timezone.
152180
153- >>> _ensure_datetime_tzinfo(datetime(2015, 1, 1)).tzinfo.zone
181+ >>> _get_tz_name( _ensure_datetime_tzinfo(datetime(2015, 1, 1)))
154182 'UTC'
155183
156184 >>> tz = get_timezone("Europe/Stockholm")
157185 >>> _ensure_datetime_tzinfo(datetime(2015, 1, 1, 13, 15, tzinfo=UTC), tzinfo=tz).hour
158186 14
159187
160188 :param datetime: Datetime to augment.
161- :param tzinfo: Optional tznfo.
189+ :param tzinfo: optional tzinfo
162190 :return: datetime with tzinfo
163191 :rtype: datetime
164192 """
@@ -184,8 +212,10 @@ def _get_time(time: time | datetime | None, tzinfo: tzinfo | None = None) -> tim
184212 time = datetime .utcnow ()
185213 elif isinstance (time , (int , float )):
186214 time = datetime .utcfromtimestamp (time )
215+
187216 if time .tzinfo is None :
188217 time = time .replace (tzinfo = UTC )
218+
189219 if isinstance (time , datetime ):
190220 if tzinfo is not None :
191221 time = time .astimezone (tzinfo )
@@ -197,28 +227,40 @@ def _get_time(time: time | datetime | None, tzinfo: tzinfo | None = None) -> tim
197227 return time
198228
199229
200- def get_timezone (zone : str | _pytz . BaseTzInfo | None = None ) -> _pytz . BaseTzInfo :
230+ def get_timezone (zone : str | tzinfo | None = None ) -> tzinfo :
201231 """Looks up a timezone by name and returns it. The timezone object
202- returned comes from ``pytz`` and corresponds to the `tzinfo` interface and
203- can be used with all of the functions of Babel that operate with dates.
232+ returned comes from ``pytz`` or ``zoneinfo``, whichever is available.
233+ It corresponds to the `tzinfo` interface and can be used with all of
234+ the functions of Babel that operate with dates.
204235
205236 If a timezone is not known a :exc:`LookupError` is raised. If `zone`
206237 is ``None`` a local zone object is returned.
207238
208239 :param zone: the name of the timezone to look up. If a timezone object
209- itself is passed in, mit 's returned unchanged.
240+ itself is passed in, it 's returned unchanged.
210241 """
211242 if zone is None :
212243 return LOCALTZ
213244 if not isinstance (zone , str ):
214245 return zone
215- try :
216- return _pytz .timezone (zone )
217- except _pytz .UnknownTimeZoneError :
218- raise LookupError (f"Unknown timezone { zone } " )
219246
247+ exc = None
248+ if pytz :
249+ try :
250+ return pytz .timezone (zone )
251+ except pytz .UnknownTimeZoneError as exc :
252+ pass
253+ else :
254+ assert zoneinfo
255+ try :
256+ return zoneinfo .ZoneInfo (zone )
257+ except zoneinfo .ZoneInfoNotFoundError as exc :
258+ pass
259+
260+ raise LookupError (f"Unknown timezone { zone } " ) from exc
220261
221- def get_next_timezone_transition (zone : _pytz .BaseTzInfo | None = None , dt : _Instant = None ) -> TimezoneTransition :
262+
263+ def get_next_timezone_transition (zone : tzinfo | None = None , dt : _Instant = None ) -> TimezoneTransition :
222264 """Given a timezone it will return a :class:`TimezoneTransition` object
223265 that holds the information about the next timezone transition that's going
224266 to happen. For instance this can be used to detect when the next DST
@@ -474,7 +516,7 @@ def get_timezone_gmt(datetime: _Instant = None, width: Literal['long', 'short',
474516 >>> get_timezone_gmt(dt, locale='en', width='iso8601_short')
475517 u'+00'
476518 >>> tz = get_timezone('America/Los_Angeles')
477- >>> dt = tz.localize( datetime(2007, 4, 1, 15, 30))
519+ >>> dt = _localize(tz, datetime(2007, 4, 1, 15, 30))
478520 >>> get_timezone_gmt(dt, locale='en')
479521 u'GMT-07:00'
480522 >>> get_timezone_gmt(dt, 'short', locale='en')
@@ -608,7 +650,7 @@ def get_timezone_name(dt_or_tzinfo: _DtOrTzinfo = None, width: Literal['long', '
608650 u'PST'
609651
610652 If this function gets passed only a `tzinfo` object and no concrete
611- `datetime`, the returned display name is indenpendent of daylight savings
653+ `datetime`, the returned display name is independent of daylight savings
612654 time. This can be used for example for selecting timezones, or to set the
613655 time of events that recur across DST changes:
614656
@@ -755,12 +797,11 @@ def format_datetime(datetime: _Instant = None, format: _PredefinedTimeFormat | s
755797 >>> format_datetime(dt, locale='en_US')
756798 u'Apr 1, 2007, 3:30:00 PM'
757799
758- For any pattern requiring the display of the time-zone, the third-party
759- ``pytz`` package is needed to explicitly specify the time-zone:
800+ For any pattern requiring the display of the timezone:
760801
761802 >>> format_datetime(dt, 'full', tzinfo=get_timezone('Europe/Paris'),
762803 ... locale='fr_FR')
763- u 'dimanche 1 avril 2007 \xe0 17:30:00 heure d\u2019\xe9t\xe9 d\u2019Europe centrale'
804+ 'dimanche 1 avril 2007 à 17:30:00 heure d’été d’Europe centrale'
764805 >>> format_datetime(dt, "yyyy.MM.dd G 'at' HH:mm:ss zzz",
765806 ... tzinfo=get_timezone('US/Eastern'), locale='en')
766807 u'2007.04.01 AD at 11:30:00 EDT'
@@ -806,9 +847,9 @@ def format_time(time: time | datetime | float | None = None, format: _Predefined
806847
807848 >>> t = datetime(2007, 4, 1, 15, 30)
808849 >>> tzinfo = get_timezone('Europe/Paris')
809- >>> t = tzinfo.localize( t)
850+ >>> t = _localize(tzinfo, t)
810851 >>> format_time(t, format='full', tzinfo=tzinfo, locale='fr_FR')
811- u '15:30:00 heure d\u2019\xe9t\xe9 d\u2019Europe centrale'
852+ '15:30:00 heure d’été d’Europe centrale'
812853 >>> format_time(t, "hh 'o''clock' a, zzzz", tzinfo=get_timezone('US/Eastern'),
813854 ... locale='en')
814855 u"09 o'clock AM, Eastern Daylight Time"
@@ -841,12 +882,17 @@ def format_time(time: time | datetime | float | None = None, format: _Predefined
841882 :param tzinfo: the time-zone to apply to the time for display
842883 :param locale: a `Locale` object or a locale identifier
843884 """
885+
886+ # get reference date for if we need to find the right timezone variant
887+ # in the pattern
888+ ref_date = time .date () if isinstance (time , datetime ) else None
889+
844890 time = _get_time (time , tzinfo )
845891
846892 locale = Locale .parse (locale )
847893 if format in ('full' , 'long' , 'medium' , 'short' ):
848894 format = get_time_format (format , locale = locale )
849- return parse_pattern (format ).apply (time , locale )
895+ return parse_pattern (format ).apply (time , locale , reference_date = ref_date )
850896
851897
852898def format_skeleton (skeleton : str , datetime : _Instant = None , tzinfo : tzinfo | None = None ,
@@ -1124,7 +1170,7 @@ def format_interval(start: _Instant, end: _Instant, skeleton: str | None = None,
11241170 return _format_fallback_interval (start , end , skeleton , tzinfo , locale )
11251171
11261172
1127- def get_period_id (time : _Instant , tzinfo : _pytz . BaseTzInfo | None = None , type : Literal ['selection' ] | None = None ,
1173+ def get_period_id (time : _Instant , tzinfo : tzinfo | None = None , type : Literal ['selection' ] | None = None ,
11281174 locale : Locale | str | None = LC_TIME ) -> str :
11291175 """
11301176 Get the day period ID for a given time.
@@ -1327,18 +1373,29 @@ def __mod__(self, other: DateTimeFormat) -> str:
13271373 return NotImplemented
13281374 return self .format % other
13291375
1330- def apply (self , datetime : date | time , locale : Locale | str | None ) -> str :
1331- return self % DateTimeFormat (datetime , locale )
1376+ def apply (
1377+ self ,
1378+ datetime : date | time ,
1379+ locale : Locale | str | None ,
1380+ reference_date : date | None = None
1381+ ) -> str :
1382+ return self % DateTimeFormat (datetime , locale , reference_date )
13321383
13331384
13341385class DateTimeFormat :
13351386
1336- def __init__ (self , value : date | time , locale : Locale | str ):
1387+ def __init__ (
1388+ self ,
1389+ value : date | time ,
1390+ locale : Locale | str ,
1391+ reference_date : date | None = None
1392+ ):
13371393 assert isinstance (value , (date , datetime , time ))
13381394 if isinstance (value , (datetime , time )) and value .tzinfo is None :
13391395 value = value .replace (tzinfo = UTC )
13401396 self .value = value
13411397 self .locale = Locale .parse (locale )
1398+ self .reference_date = reference_date
13421399
13431400 def __getitem__ (self , name : str ) -> str :
13441401 char = name [0 ]
@@ -1558,46 +1615,54 @@ def format_milliseconds_in_day(self, num):
15581615
15591616 def format_timezone (self , char : str , num : int ) -> str :
15601617 width = {3 : 'short' , 4 : 'long' , 5 : 'iso8601' }[max (3 , num )]
1618+
1619+ # It could be that we only receive a time to format, but also have a
1620+ # reference date which is important to distinguish between timezone
1621+ # variants (summer/standard time)
1622+ value = self .value
1623+ if self .reference_date :
1624+ value = datetime .combine (self .reference_date , self .value )
1625+
15611626 if char == 'z' :
1562- return get_timezone_name (self . value , width , locale = self .locale )
1627+ return get_timezone_name (value , width , locale = self .locale )
15631628 elif char == 'Z' :
15641629 if num == 5 :
1565- return get_timezone_gmt (self . value , width , locale = self .locale , return_z = True )
1566- return get_timezone_gmt (self . value , width , locale = self .locale )
1630+ return get_timezone_gmt (value , width , locale = self .locale , return_z = True )
1631+ return get_timezone_gmt (value , width , locale = self .locale )
15671632 elif char == 'O' :
15681633 if num == 4 :
1569- return get_timezone_gmt (self . value , width , locale = self .locale )
1634+ return get_timezone_gmt (value , width , locale = self .locale )
15701635 # TODO: To add support for O:1
15711636 elif char == 'v' :
1572- return get_timezone_name (self . value .tzinfo , width ,
1637+ return get_timezone_name (value .tzinfo , width ,
15731638 locale = self .locale )
15741639 elif char == 'V' :
15751640 if num == 1 :
1576- return get_timezone_name (self . value .tzinfo , width ,
1641+ return get_timezone_name (value .tzinfo , width ,
15771642 uncommon = True , locale = self .locale )
15781643 elif num == 2 :
1579- return get_timezone_name (self . value .tzinfo , locale = self .locale , return_zone = True )
1644+ return get_timezone_name (value .tzinfo , locale = self .locale , return_zone = True )
15801645 elif num == 3 :
1581- return get_timezone_location (self . value .tzinfo , locale = self .locale , return_city = True )
1582- return get_timezone_location (self . value .tzinfo , locale = self .locale )
1646+ return get_timezone_location (value .tzinfo , locale = self .locale , return_city = True )
1647+ return get_timezone_location (value .tzinfo , locale = self .locale )
15831648 # Included additional elif condition to add support for 'Xx' in timezone format
15841649 elif char == 'X' :
15851650 if num == 1 :
1586- return get_timezone_gmt (self . value , width = 'iso8601_short' , locale = self .locale ,
1651+ return get_timezone_gmt (value , width = 'iso8601_short' , locale = self .locale ,
15871652 return_z = True )
15881653 elif num in (2 , 4 ):
1589- return get_timezone_gmt (self . value , width = 'short' , locale = self .locale ,
1654+ return get_timezone_gmt (value , width = 'short' , locale = self .locale ,
15901655 return_z = True )
15911656 elif num in (3 , 5 ):
1592- return get_timezone_gmt (self . value , width = 'iso8601' , locale = self .locale ,
1657+ return get_timezone_gmt (value , width = 'iso8601' , locale = self .locale ,
15931658 return_z = True )
15941659 elif char == 'x' :
15951660 if num == 1 :
1596- return get_timezone_gmt (self . value , width = 'iso8601_short' , locale = self .locale )
1661+ return get_timezone_gmt (value , width = 'iso8601_short' , locale = self .locale )
15971662 elif num in (2 , 4 ):
1598- return get_timezone_gmt (self . value , width = 'short' , locale = self .locale )
1663+ return get_timezone_gmt (value , width = 'short' , locale = self .locale )
15991664 elif num in (3 , 5 ):
1600- return get_timezone_gmt (self . value , width = 'iso8601' , locale = self .locale )
1665+ return get_timezone_gmt (value , width = 'iso8601' , locale = self .locale )
16011666
16021667 def format (self , value : SupportsInt , length : int ) -> str :
16031668 return '%0*d' % (length , value )
0 commit comments