19
19
20
20
import re
21
21
import warnings
22
+ from typing import TYPE_CHECKING , SupportsInt
23
+
24
+ try :
25
+ import pytz
26
+ except ModuleNotFoundError :
27
+ pytz = None
28
+ import zoneinfo
29
+
22
30
from bisect import bisect_right
23
31
from collections .abc import Iterable
24
32
from datetime import date , datetime , time , timedelta , tzinfo
25
- from typing import TYPE_CHECKING , SupportsInt
26
-
27
- import pytz as _pytz
28
33
34
+ from babel import localtime
29
35
from babel .core import Locale , default_locale , get_global
30
36
from babel .localedata import LocaleDataDict
31
- from babel .util import LOCALTZ , UTC
32
37
33
38
if TYPE_CHECKING :
34
39
from typing_extensions import Literal , TypeAlias
35
-
36
40
_Instant : TypeAlias = date | time | float | None
37
41
_PredefinedTimeFormat : TypeAlias = Literal ['full' , 'long' , 'medium' , 'short' ]
38
42
_Context : TypeAlias = Literal ['format' , 'stand-alone' ]
48
52
NO_INHERITANCE_MARKER = u'\u2205 \u2205 \u2205 '
49
53
50
54
55
+ if pytz :
56
+ UTC = pytz .utc
57
+ else :
58
+ UTC = zoneinfo .ZoneInfo ('UTC' )
59
+ LOCALTZ = localtime .LOCALTZ
60
+
51
61
LC_TIME = default_locale ('LC_TIME' )
52
62
53
63
# Aliases for use in scopes where the modules are shadowed by local variables
56
66
time_ = time
57
67
58
68
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
+
59
87
def _get_dt_and_tzinfo (dt_or_tzinfo : _DtOrTzinfo ) -> tuple [datetime_ | None , tzinfo ]:
60
88
"""
61
89
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) -
150
178
151
179
If a tzinfo is passed in, the datetime is normalized to that timezone.
152
180
153
- >>> _ensure_datetime_tzinfo(datetime(2015, 1, 1)).tzinfo.zone
181
+ >>> _get_tz_name( _ensure_datetime_tzinfo(datetime(2015, 1, 1)))
154
182
'UTC'
155
183
156
184
>>> tz = get_timezone("Europe/Stockholm")
157
185
>>> _ensure_datetime_tzinfo(datetime(2015, 1, 1, 13, 15, tzinfo=UTC), tzinfo=tz).hour
158
186
14
159
187
160
188
:param datetime: Datetime to augment.
161
- :param tzinfo: Optional tznfo.
189
+ :param tzinfo: optional tzinfo
162
190
:return: datetime with tzinfo
163
191
:rtype: datetime
164
192
"""
@@ -184,8 +212,10 @@ def _get_time(time: time | datetime | None, tzinfo: tzinfo | None = None) -> tim
184
212
time = datetime .utcnow ()
185
213
elif isinstance (time , (int , float )):
186
214
time = datetime .utcfromtimestamp (time )
215
+
187
216
if time .tzinfo is None :
188
217
time = time .replace (tzinfo = UTC )
218
+
189
219
if isinstance (time , datetime ):
190
220
if tzinfo is not None :
191
221
time = time .astimezone (tzinfo )
@@ -197,28 +227,40 @@ def _get_time(time: time | datetime | None, tzinfo: tzinfo | None = None) -> tim
197
227
return time
198
228
199
229
200
- def get_timezone (zone : str | _pytz . BaseTzInfo | None = None ) -> _pytz . BaseTzInfo :
230
+ def get_timezone (zone : str | tzinfo | None = None ) -> tzinfo :
201
231
"""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.
204
235
205
236
If a timezone is not known a :exc:`LookupError` is raised. If `zone`
206
237
is ``None`` a local zone object is returned.
207
238
208
239
: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.
210
241
"""
211
242
if zone is None :
212
243
return LOCALTZ
213
244
if not isinstance (zone , str ):
214
245
return zone
215
- try :
216
- return _pytz .timezone (zone )
217
- except _pytz .UnknownTimeZoneError :
218
- raise LookupError (f"Unknown timezone { zone } " )
219
246
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
220
261
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 :
222
264
"""Given a timezone it will return a :class:`TimezoneTransition` object
223
265
that holds the information about the next timezone transition that's going
224
266
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',
474
516
>>> get_timezone_gmt(dt, locale='en', width='iso8601_short')
475
517
u'+00'
476
518
>>> 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))
478
520
>>> get_timezone_gmt(dt, locale='en')
479
521
u'GMT-07:00'
480
522
>>> get_timezone_gmt(dt, 'short', locale='en')
@@ -608,7 +650,7 @@ def get_timezone_name(dt_or_tzinfo: _DtOrTzinfo = None, width: Literal['long', '
608
650
u'PST'
609
651
610
652
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
612
654
time. This can be used for example for selecting timezones, or to set the
613
655
time of events that recur across DST changes:
614
656
@@ -755,12 +797,11 @@ def format_datetime(datetime: _Instant = None, format: _PredefinedTimeFormat | s
755
797
>>> format_datetime(dt, locale='en_US')
756
798
u'Apr 1, 2007, 3:30:00 PM'
757
799
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:
760
801
761
802
>>> format_datetime(dt, 'full', tzinfo=get_timezone('Europe/Paris'),
762
803
... 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'
764
805
>>> format_datetime(dt, "yyyy.MM.dd G 'at' HH:mm:ss zzz",
765
806
... tzinfo=get_timezone('US/Eastern'), locale='en')
766
807
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
806
847
807
848
>>> t = datetime(2007, 4, 1, 15, 30)
808
849
>>> tzinfo = get_timezone('Europe/Paris')
809
- >>> t = tzinfo.localize( t)
850
+ >>> t = _localize(tzinfo, t)
810
851
>>> 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'
812
853
>>> format_time(t, "hh 'o''clock' a, zzzz", tzinfo=get_timezone('US/Eastern'),
813
854
... locale='en')
814
855
u"09 o'clock AM, Eastern Daylight Time"
@@ -841,12 +882,17 @@ def format_time(time: time | datetime | float | None = None, format: _Predefined
841
882
:param tzinfo: the time-zone to apply to the time for display
842
883
:param locale: a `Locale` object or a locale identifier
843
884
"""
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
+
844
890
time = _get_time (time , tzinfo )
845
891
846
892
locale = Locale .parse (locale )
847
893
if format in ('full' , 'long' , 'medium' , 'short' ):
848
894
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 )
850
896
851
897
852
898
def 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,
1124
1170
return _format_fallback_interval (start , end , skeleton , tzinfo , locale )
1125
1171
1126
1172
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 ,
1128
1174
locale : Locale | str | None = LC_TIME ) -> str :
1129
1175
"""
1130
1176
Get the day period ID for a given time.
@@ -1327,18 +1373,29 @@ def __mod__(self, other: DateTimeFormat) -> str:
1327
1373
return NotImplemented
1328
1374
return self .format % other
1329
1375
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 )
1332
1383
1333
1384
1334
1385
class DateTimeFormat :
1335
1386
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
+ ):
1337
1393
assert isinstance (value , (date , datetime , time ))
1338
1394
if isinstance (value , (datetime , time )) and value .tzinfo is None :
1339
1395
value = value .replace (tzinfo = UTC )
1340
1396
self .value = value
1341
1397
self .locale = Locale .parse (locale )
1398
+ self .reference_date = reference_date
1342
1399
1343
1400
def __getitem__ (self , name : str ) -> str :
1344
1401
char = name [0 ]
@@ -1558,46 +1615,54 @@ def format_milliseconds_in_day(self, num):
1558
1615
1559
1616
def format_timezone (self , char : str , num : int ) -> str :
1560
1617
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
+
1561
1626
if char == 'z' :
1562
- return get_timezone_name (self . value , width , locale = self .locale )
1627
+ return get_timezone_name (value , width , locale = self .locale )
1563
1628
elif char == 'Z' :
1564
1629
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 )
1567
1632
elif char == 'O' :
1568
1633
if num == 4 :
1569
- return get_timezone_gmt (self . value , width , locale = self .locale )
1634
+ return get_timezone_gmt (value , width , locale = self .locale )
1570
1635
# TODO: To add support for O:1
1571
1636
elif char == 'v' :
1572
- return get_timezone_name (self . value .tzinfo , width ,
1637
+ return get_timezone_name (value .tzinfo , width ,
1573
1638
locale = self .locale )
1574
1639
elif char == 'V' :
1575
1640
if num == 1 :
1576
- return get_timezone_name (self . value .tzinfo , width ,
1641
+ return get_timezone_name (value .tzinfo , width ,
1577
1642
uncommon = True , locale = self .locale )
1578
1643
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 )
1580
1645
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 )
1583
1648
# Included additional elif condition to add support for 'Xx' in timezone format
1584
1649
elif char == 'X' :
1585
1650
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 ,
1587
1652
return_z = True )
1588
1653
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 ,
1590
1655
return_z = True )
1591
1656
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 ,
1593
1658
return_z = True )
1594
1659
elif char == 'x' :
1595
1660
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 )
1597
1662
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 )
1599
1664
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 )
1601
1666
1602
1667
def format (self , value : SupportsInt , length : int ) -> str :
1603
1668
return '%0*d' % (length , value )
0 commit comments