]> git.ipfire.org Git - thirdparty/babel.git/commitdiff
Implement zoneinfo support and make pytz optional (#940)
authorDS/Charlie <82801887+ds-cbo@users.noreply.github.com>
Wed, 11 Jan 2023 10:34:43 +0000 (11:34 +0100)
committerGitHub <noreply@github.com>
Wed, 11 Jan 2023 10:34:43 +0000 (12:34 +0200)
15 files changed:
CHANGES.rst
babel/dates.py
babel/localtime/__init__.py
babel/localtime/_helpers.py [new file with mode: 0644]
babel/localtime/_unix.py
babel/localtime/_win32.py
babel/support.py
babel/util.py
docs/dates.rst
docs/dev.rst
docs/installation.rst
setup.py
tests/test_date_intervals.py
tests/test_dates.py
tests/test_support.py

index 949d16dd7d16e80e28b4ea3151984e05a6e428da..bee5c3f406938752f27b42d59e85dfbb5fcbc4fc 100644 (file)
@@ -1,6 +1,13 @@
 Babel Changelog
 ===============
 
+Unreleased
+----------
+
+* Use `zoneinfo` timezone resolving on python 3.9+, while keeping pytz support
+  for lower versions
+
+
 Version 2.11.0
 --------------
 
index 27f3fe648a0e6ec67c59b2b3995d5202acb5c416..f7289619f7ae40ce78cfa4a76f5a1eec5a037ffd 100644 (file)
@@ -19,20 +19,24 @@ from __future__ import annotations
 
 import re
 import warnings
+from typing import TYPE_CHECKING, SupportsInt
+
+try:
+    import pytz
+except ModuleNotFoundError:
+    pytz = None
+    import zoneinfo
+
 from bisect import bisect_right
 from collections.abc import Iterable
 from datetime import date, datetime, time, timedelta, tzinfo
-from typing import TYPE_CHECKING, SupportsInt
-
-import pytz as _pytz
 
+from babel import localtime
 from babel.core import Locale, default_locale, get_global
 from babel.localedata import LocaleDataDict
-from babel.util import LOCALTZ, UTC
 
 if TYPE_CHECKING:
     from typing_extensions import Literal, TypeAlias
-
     _Instant: TypeAlias = date | time | float | None
     _PredefinedTimeFormat: TypeAlias = Literal['full', 'long', 'medium', 'short']
     _Context: TypeAlias = Literal['format', 'stand-alone']
@@ -48,6 +52,12 @@ if TYPE_CHECKING:
 NO_INHERITANCE_MARKER = u'\u2205\u2205\u2205'
 
 
+if pytz:
+    UTC = pytz.utc
+else:
+    UTC = zoneinfo.ZoneInfo('UTC')
+LOCALTZ = localtime.LOCALTZ
+
 LC_TIME = default_locale('LC_TIME')
 
 # Aliases for use in scopes where the modules are shadowed by local variables
@@ -56,6 +66,24 @@ datetime_ = datetime
 time_ = time
 
 
+def _localize(tz: tzinfo, dt: datetime) -> datetime:
+    # Support localizing with both pytz and zoneinfo tzinfos
+    # nothing to do
+    if dt.tzinfo is tz:
+        return dt
+
+    if hasattr(tz, 'localize'):  # pytz
+        return tz.localize(dt)
+
+    if dt.tzinfo is None:
+        # convert naive to localized
+        return dt.replace(tzinfo=tz)
+
+    # convert timezones
+    return dt.astimezone(tz)
+
+
+
 def _get_dt_and_tzinfo(dt_or_tzinfo: _DtOrTzinfo) -> tuple[datetime_ | None, tzinfo]:
     """
     Parse a `dt_or_tzinfo` value into a datetime and a tzinfo.
@@ -150,7 +178,7 @@ def _ensure_datetime_tzinfo(datetime: datetime_, tzinfo: tzinfo | None = None) -
 
     If a tzinfo is passed in, the datetime is normalized to that timezone.
 
-    >>> _ensure_datetime_tzinfo(datetime(2015, 1, 1)).tzinfo.zone
+    >>> _get_tz_name(_ensure_datetime_tzinfo(datetime(2015, 1, 1)))
     'UTC'
 
     >>> tz = get_timezone("Europe/Stockholm")
@@ -158,7 +186,7 @@ def _ensure_datetime_tzinfo(datetime: datetime_, tzinfo: tzinfo | None = None) -
     14
 
     :param datetime: Datetime to augment.
-    :param tzinfo: Optional tznfo.
+    :param tzinfo: optional tzinfo
     :return: datetime with tzinfo
     :rtype: datetime
     """
@@ -184,8 +212,10 @@ def _get_time(time: time | datetime | None, tzinfo: tzinfo | None = None) -> tim
         time = datetime.utcnow()
     elif isinstance(time, (int, float)):
         time = datetime.utcfromtimestamp(time)
+
     if time.tzinfo is None:
         time = time.replace(tzinfo=UTC)
+
     if isinstance(time, datetime):
         if tzinfo is not None:
             time = time.astimezone(tzinfo)
@@ -197,28 +227,40 @@ def _get_time(time: time | datetime | None, tzinfo: tzinfo | None = None) -> tim
     return time
 
 
-def get_timezone(zone: str | _pytz.BaseTzInfo | None = None) -> _pytz.BaseTzInfo:
+def get_timezone(zone: str | tzinfo | None = None) -> tzinfo:
     """Looks up a timezone by name and returns it.  The timezone object
-    returned comes from ``pytz`` and corresponds to the `tzinfo` interface and
-    can be used with all of the functions of Babel that operate with dates.
+    returned comes from ``pytz`` or ``zoneinfo``, whichever is available.
+    It corresponds to the `tzinfo` interface and can be used with all of
+    the functions of Babel that operate with dates.
 
     If a timezone is not known a :exc:`LookupError` is raised.  If `zone`
     is ``None`` a local zone object is returned.
 
     :param zone: the name of the timezone to look up.  If a timezone object
-                 itself is passed in, mit's returned unchanged.
+                 itself is passed in, it's returned unchanged.
     """
     if zone is None:
         return LOCALTZ
     if not isinstance(zone, str):
         return zone
-    try:
-        return _pytz.timezone(zone)
-    except _pytz.UnknownTimeZoneError:
-        raise LookupError(f"Unknown timezone {zone}")
 
+    exc = None
+    if pytz:
+        try:
+            return pytz.timezone(zone)
+        except pytz.UnknownTimeZoneError as exc:
+            pass
+    else:
+        assert zoneinfo
+        try:
+            return zoneinfo.ZoneInfo(zone)
+        except zoneinfo.ZoneInfoNotFoundError as exc:
+            pass
+
+    raise LookupError(f"Unknown timezone {zone}") from exc
 
-def get_next_timezone_transition(zone: _pytz.BaseTzInfo | None = None, dt: _Instant = None) -> TimezoneTransition:
+
+def get_next_timezone_transition(zone: tzinfo | None = None, dt: _Instant = None) -> TimezoneTransition:
     """Given a timezone it will return a :class:`TimezoneTransition` object
     that holds the information about the next timezone transition that's going
     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',
     >>> get_timezone_gmt(dt, locale='en', width='iso8601_short')
     u'+00'
     >>> tz = get_timezone('America/Los_Angeles')
-    >>> dt = tz.localize(datetime(2007, 4, 1, 15, 30))
+    >>> dt = _localize(tz, datetime(2007, 4, 1, 15, 30))
     >>> get_timezone_gmt(dt, locale='en')
     u'GMT-07:00'
     >>> get_timezone_gmt(dt, 'short', locale='en')
@@ -608,7 +650,7 @@ def get_timezone_name(dt_or_tzinfo: _DtOrTzinfo = None, width: Literal['long', '
     u'PST'
 
     If this function gets passed only a `tzinfo` object and no concrete
-    `datetime`,  the returned display name is indenpendent of daylight savings
+    `datetime`,  the returned display name is independent of daylight savings
     time. This can be used for example for selecting timezones, or to set the
     time of events that recur across DST changes:
 
@@ -755,12 +797,11 @@ def format_datetime(datetime: _Instant = None, format: _PredefinedTimeFormat | s
     >>> format_datetime(dt, locale='en_US')
     u'Apr 1, 2007, 3:30:00 PM'
 
-    For any pattern requiring the display of the time-zone, the third-party
-    ``pytz`` package is needed to explicitly specify the time-zone:
+    For any pattern requiring the display of the timezone:
 
     >>> format_datetime(dt, 'full', tzinfo=get_timezone('Europe/Paris'),
     ...                 locale='fr_FR')
-    u'dimanche 1 avril 2007 \xe0 17:30:00 heure d\u2019\xe9t\xe9 d\u2019Europe centrale'
+    'dimanche 1 avril 2007 à 17:30:00 heure d’été d’Europe centrale'
     >>> format_datetime(dt, "yyyy.MM.dd G 'at' HH:mm:ss zzz",
     ...                 tzinfo=get_timezone('US/Eastern'), locale='en')
     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
 
     >>> t = datetime(2007, 4, 1, 15, 30)
     >>> tzinfo = get_timezone('Europe/Paris')
-    >>> t = tzinfo.localize(t)
+    >>> t = _localize(tzinfo, t)
     >>> format_time(t, format='full', tzinfo=tzinfo, locale='fr_FR')
-    u'15:30:00 heure d\u2019\xe9t\xe9 d\u2019Europe centrale'
+    '15:30:00 heure d’été d’Europe centrale'
     >>> format_time(t, "hh 'o''clock' a, zzzz", tzinfo=get_timezone('US/Eastern'),
     ...             locale='en')
     u"09 o'clock AM, Eastern Daylight Time"
@@ -841,12 +882,17 @@ def format_time(time: time | datetime | float | None = None, format: _Predefined
     :param tzinfo: the time-zone to apply to the time for display
     :param locale: a `Locale` object or a locale identifier
     """
+
+    # get reference date for if we need to find the right timezone variant
+    # in the pattern
+    ref_date = time.date() if isinstance(time, datetime) else None
+
     time = _get_time(time, tzinfo)
 
     locale = Locale.parse(locale)
     if format in ('full', 'long', 'medium', 'short'):
         format = get_time_format(format, locale=locale)
-    return parse_pattern(format).apply(time, locale)
+    return parse_pattern(format).apply(time, locale, reference_date=ref_date)
 
 
 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,
     return _format_fallback_interval(start, end, skeleton, tzinfo, locale)
 
 
-def get_period_id(time: _Instant, tzinfo: _pytz.BaseTzInfo | None = None, type: Literal['selection'] | None = None,
+def get_period_id(time: _Instant, tzinfo: tzinfo | None = None, type: Literal['selection'] | None = None,
                   locale: Locale | str | None = LC_TIME) -> str:
     """
     Get the day period ID for a given time.
@@ -1327,18 +1373,29 @@ class DateTimePattern:
             return NotImplemented
         return self.format % other
 
-    def apply(self, datetime: date | time, locale: Locale | str | None) -> str:
-        return self % DateTimeFormat(datetime, locale)
+    def apply(
+        self,
+        datetime: date | time,
+        locale: Locale | str | None,
+        reference_date: date | None = None
+    ) -> str:
+        return self % DateTimeFormat(datetime, locale, reference_date)
 
 
 class DateTimeFormat:
 
-    def __init__(self, value: date | time, locale: Locale | str):
+    def __init__(
+        self,
+        value: date | time,
+        locale: Locale | str,
+        reference_date: date | None = None
+    ):
         assert isinstance(value, (date, datetime, time))
         if isinstance(value, (datetime, time)) and value.tzinfo is None:
             value = value.replace(tzinfo=UTC)
         self.value = value
         self.locale = Locale.parse(locale)
+        self.reference_date = reference_date
 
     def __getitem__(self, name: str) -> str:
         char = name[0]
@@ -1558,46 +1615,54 @@ class DateTimeFormat:
 
     def format_timezone(self, char: str, num: int) -> str:
         width = {3: 'short', 4: 'long', 5: 'iso8601'}[max(3, num)]
+
+        # It could be that we only receive a time to format, but also have a
+        # reference date which is important to distinguish between timezone
+        # variants (summer/standard time)
+        value = self.value
+        if self.reference_date:
+            value = datetime.combine(self.reference_date, self.value)
+
         if char == 'z':
-            return get_timezone_name(self.value, width, locale=self.locale)
+            return get_timezone_name(value, width, locale=self.locale)
         elif char == 'Z':
             if num == 5:
-                return get_timezone_gmt(self.value, width, locale=self.locale, return_z=True)
-            return get_timezone_gmt(self.value, width, locale=self.locale)
+                return get_timezone_gmt(value, width, locale=self.locale, return_z=True)
+            return get_timezone_gmt(value, width, locale=self.locale)
         elif char == 'O':
             if num == 4:
-                return get_timezone_gmt(self.value, width, locale=self.locale)
+                return get_timezone_gmt(value, width, locale=self.locale)
         # TODO: To add support for O:1
         elif char == 'v':
-            return get_timezone_name(self.value.tzinfo, width,
+            return get_timezone_name(value.tzinfo, width,
                                      locale=self.locale)
         elif char == 'V':
             if num == 1:
-                return get_timezone_name(self.value.tzinfo, width,
+                return get_timezone_name(value.tzinfo, width,
                                          uncommon=True, locale=self.locale)
             elif num == 2:
-                return get_timezone_name(self.value.tzinfo, locale=self.locale, return_zone=True)
+                return get_timezone_name(value.tzinfo, locale=self.locale, return_zone=True)
             elif num == 3:
-                return get_timezone_location(self.value.tzinfo, locale=self.locale, return_city=True)
-            return get_timezone_location(self.value.tzinfo, locale=self.locale)
+                return get_timezone_location(value.tzinfo, locale=self.locale, return_city=True)
+            return get_timezone_location(value.tzinfo, locale=self.locale)
         # Included additional elif condition to add support for 'Xx' in timezone format
         elif char == 'X':
             if num == 1:
-                return get_timezone_gmt(self.value, width='iso8601_short', locale=self.locale,
+                return get_timezone_gmt(value, width='iso8601_short', locale=self.locale,
                                         return_z=True)
             elif num in (2, 4):
-                return get_timezone_gmt(self.value, width='short', locale=self.locale,
+                return get_timezone_gmt(value, width='short', locale=self.locale,
                                         return_z=True)
             elif num in (3, 5):
-                return get_timezone_gmt(self.value, width='iso8601', locale=self.locale,
+                return get_timezone_gmt(value, width='iso8601', locale=self.locale,
                                         return_z=True)
         elif char == 'x':
             if num == 1:
-                return get_timezone_gmt(self.value, width='iso8601_short', locale=self.locale)
+                return get_timezone_gmt(value, width='iso8601_short', locale=self.locale)
             elif num in (2, 4):
-                return get_timezone_gmt(self.value, width='short', locale=self.locale)
+                return get_timezone_gmt(value, width='short', locale=self.locale)
             elif num in (3, 5):
-                return get_timezone_gmt(self.value, width='iso8601', locale=self.locale)
+                return get_timezone_gmt(value, width='iso8601', locale=self.locale)
 
     def format(self, value: SupportsInt, length: int) -> str:
         return '%0*d' % (length, value)
index ffe2d49f10be8e903cbcbc4ca15ca8a8fb2dd79c..c7f214aa83496569f3f40fc31fcb27f3d424cdee 100644 (file)
@@ -14,8 +14,6 @@ import time
 from datetime import datetime, timedelta, tzinfo
 from threading import RLock
 
-import pytz
-
 if sys.platform == 'win32':
     from babel.localtime._win32 import _get_localzone
 else:
@@ -61,7 +59,7 @@ class _FallbackLocalTimezone(tzinfo):
         return tt.tm_isdst > 0
 
 
-def get_localzone() -> pytz.BaseTzInfo:
+def get_localzone() -> tzinfo:
     """Returns the current underlying local timezone object.
     Generally this function does not need to be used, it's a
     better idea to use the :data:`LOCALTZ` singleton instead.
@@ -71,5 +69,5 @@ def get_localzone() -> pytz.BaseTzInfo:
 
 try:
     LOCALTZ = get_localzone()
-except pytz.UnknownTimeZoneError:
+except LookupError:
     LOCALTZ = _FallbackLocalTimezone()
diff --git a/babel/localtime/_helpers.py b/babel/localtime/_helpers.py
new file mode 100644 (file)
index 0000000..b7238f6
--- /dev/null
@@ -0,0 +1,42 @@
+try:
+    import pytz
+except ModuleNotFoundError:
+    pytz = None
+    import zoneinfo
+
+
+def _get_tzinfo(tzenv: str):
+    """Get the tzinfo from `zoneinfo` or `pytz`
+
+    :param tzenv: timezone in the form of Continent/City
+    :return: tzinfo object or None if not found
+    """
+    if pytz:
+        try:
+            return pytz.timezone(tzenv)
+        except pytz.UnknownTimeZoneError:
+            pass
+    else:
+        try:
+            return zoneinfo.ZoneInfo(tzenv)
+        except zoneinfo.ZoneInfoNotFoundError:
+            pass
+
+    return None
+
+def _get_tzinfo_or_raise(tzenv: str):
+    tzinfo = _get_tzinfo(tzenv)
+    if tzinfo is None:
+        raise LookupError(
+            f"Can not find timezone {tzenv}. \n"
+            "Timezone names are generally in the form `Continent/City`."
+        )
+    return tzinfo
+
+
+def _get_tzinfo_from_file(tzfilename: str):
+    with open(tzfilename, 'rb') as tzfile:
+        if pytz:
+            return pytz.tzfile.build_tzinfo('local', tzfile)
+        else:
+            return zoneinfo.ZoneInfo.from_file(tzfile)
index beb7f6021e4269ed84061083c600efd04fcd1b11..319c8cfbc5b4f32a003152303df6b155c2a07f87 100644 (file)
@@ -1,33 +1,31 @@
 import os
 import re
-import pytz
 
+from datetime import tzinfo
 
-def _tz_from_env(tzenv: str) -> pytz.BaseTzInfo:
+from babel.localtime._helpers import (
+    _get_tzinfo_from_file,
+    _get_tzinfo_or_raise,
+    _get_tzinfo,
+)
+
+def _tz_from_env(tzenv: str) -> tzinfo:
     if tzenv[0] == ':':
         tzenv = tzenv[1:]
 
     # TZ specifies a file
     if os.path.exists(tzenv):
-        with open(tzenv, 'rb') as tzfile:
-            return pytz.tzfile.build_tzinfo('local', tzfile)
+        return _get_tzinfo_from_file(tzenv)
 
     # TZ specifies a zoneinfo zone.
-    try:
-        tz = pytz.timezone(tzenv)
-        # That worked, so we return this:
-        return tz
-    except pytz.UnknownTimeZoneError:
-        raise pytz.UnknownTimeZoneError(
-            "tzlocal() does not support non-zoneinfo timezones like %s. \n"
-            "Please use a timezone in the form of Continent/City")
+    return _get_tzinfo_or_raise(tzenv)
 
 
-def _get_localzone(_root: str = '/') -> pytz.BaseTzInfo:
+def _get_localzone(_root: str = '/') -> tzinfo:
     """Tries to find the local timezone configuration.
-    This method prefers finding the timezone name and passing that to pytz,
-    over passing in the localtime file, as in the later case the zoneinfo
-    name is unknown.
+    This method prefers finding the timezone name and passing that to
+    zoneinfo or pytz, over passing in the localtime file, as in the later
+    case the zoneinfo name is unknown.
     The parameter _root makes the function look for files like /etc/localtime
     beneath the _root directory. This is primarily used by the tests.
     In normal usage you call the function without parameters.
@@ -48,10 +46,9 @@ def _get_localzone(_root: str = '/') -> pytz.BaseTzInfo:
         pos = link_dst.find('/zoneinfo/')
         if pos >= 0:
             zone_name = link_dst[pos + 10:]
-            try:
-                return pytz.timezone(zone_name)
-            except pytz.UnknownTimeZoneError:
-                pass
+            tzinfo = _get_tzinfo(zone_name)
+            if tzinfo is not None:
+                return tzinfo
 
     # Now look for distribution specific configuration files
     # that contain the timezone name.
@@ -69,7 +66,8 @@ def _get_localzone(_root: str = '/') -> pytz.BaseTzInfo:
                     etctz, dummy = etctz.split(' ', 1)
                 if '#' in etctz:
                     etctz, dummy = etctz.split('#', 1)
-                return pytz.timezone(etctz.replace(' ', '_'))
+
+                return _get_tzinfo_or_raise(etctz.replace(' ', '_'))
 
     # CentOS has a ZONE setting in /etc/sysconfig/clock,
     # OpenSUSE has a TIMEZONE setting in /etc/sysconfig/clock and
@@ -87,7 +85,7 @@ def _get_localzone(_root: str = '/') -> pytz.BaseTzInfo:
                 if match is not None:
                     # We found a timezone
                     etctz = match.group("etctz")
-                    return pytz.timezone(etctz.replace(' ', '_'))
+                    return _get_tzinfo_or_raise(etctz.replace(' ', '_'))
 
     # No explicit setting existed. Use localtime
     for filename in ('etc/localtime', 'usr/local/etc/localtime'):
@@ -95,8 +93,6 @@ def _get_localzone(_root: str = '/') -> pytz.BaseTzInfo:
 
         if not os.path.exists(tzpath):
             continue
+        return _get_tzinfo_from_file(tzpath)
 
-        with open(tzpath, 'rb') as tzfile:
-            return pytz.tzfile.build_tzinfo('local', tzfile)
-
-    raise pytz.UnknownTimeZoneError('Can not find any timezone configuration')
+    raise LookupError('Can not find any timezone configuration')
index 98d51708d6a2b474d08576093cc58f124493bfd3..3d7e0d512b475f97b462b96f3f4a48c2e61b9ddc 100644 (file)
@@ -5,11 +5,10 @@ try:
 except ImportError:
     winreg = None
 
-from typing import Any, Dict, cast
-
-import pytz
-
+from datetime import tzinfo
 from babel.core import get_global
+from babel.localtime._helpers import _get_tzinfo_or_raise
+from typing import Any, Dict, cast
 
 # When building the cldr data on windows this module gets imported.
 # Because at that point there is no global.dat yet this call will
@@ -85,13 +84,14 @@ def get_localzone_name() -> str:
 
     # Return what we have.
     if timezone is None:
-        raise pytz.UnknownTimeZoneError(f"Can not find timezone {tzkeyname}")
+        raise LookupError(f"Can not find timezone {tzkeyname}")
 
     return timezone
 
 
-def _get_localzone() -> pytz.BaseTzInfo:
+def _get_localzone() -> tzinfo:
     if winreg is None:
-        raise pytz.UnknownTimeZoneError(
+        raise LookupError(
             'Runtime support not available')
-    return pytz.timezone(get_localzone_name())
+
+    return _get_tzinfo_or_raise(get_localzone_name())
index 7a6eaa23affc459c2a2142f33f9f41e13743d4c4..7477ee10c9a746210624c12f1e585613c99945cd 100644 (file)
@@ -17,14 +17,19 @@ import gettext
 import locale
 import os
 from collections.abc import Iterator
-from datetime import date as _date, datetime as _datetime, time as _time, timedelta as _timedelta
+from datetime import (
+    date as _date,
+    datetime as _datetime,
+    time as _time,
+    timedelta as _timedelta,
+    tzinfo
+)
 from typing import TYPE_CHECKING, Any, Callable
 
-from pytz import BaseTzInfo
-
 from babel.core import Locale
+
 from babel.dates import (format_date, format_datetime, format_time,
-                         format_timedelta)
+                         format_timedelta, get_timezone)
 from babel.numbers import (format_compact_currency, format_compact_decimal,
                            format_currency, format_decimal, format_percent,
                            format_scientific)
@@ -47,7 +52,7 @@ class Format:
     u'1.234'
     """
 
-    def __init__(self, locale: Locale | str, tzinfo: BaseTzInfo | None = None) -> None:
+    def __init__(self, locale: Locale | str, tzinfo: tzinfo | None = None) -> None:
         """Initialize the formatter.
 
         :param locale: the locale identifier or `Locale` instance
@@ -70,8 +75,7 @@ class Format:
         """Return a date and time formatted according to the given pattern.
 
         >>> from datetime import datetime
-        >>> from pytz import timezone
-        >>> fmt = Format('en_US', tzinfo=timezone('US/Eastern'))
+        >>> fmt = Format('en_US', tzinfo=get_timezone('US/Eastern'))
         >>> fmt.datetime(datetime(2007, 4, 1, 15, 30))
         u'Apr 1, 2007, 11:30:00 AM'
         """
@@ -82,8 +86,7 @@ class Format:
         """Return a time formatted according to the given pattern.
 
         >>> from datetime import datetime
-        >>> from pytz import timezone
-        >>> fmt = Format('en_US', tzinfo=timezone('US/Eastern'))
+        >>> fmt = Format('en_US', tzinfo=get_timezone('US/Eastern'))
         >>> fmt.time(datetime(2007, 4, 1, 15, 30))
         u'11:30:00 AM'
         """
index f159c33c1cc08ea4c50d4efbe4341e11f5498e23..fb4870d6687b2a655c16c0c9c791eb14ae107fec 100644 (file)
@@ -14,14 +14,12 @@ import collections
 import os
 import re
 import textwrap
+from babel import localtime, dates
+
 from collections.abc import Generator, Iterable
 from datetime import datetime as datetime_, timedelta, tzinfo
 from typing import IO, Any, TypeVar
 
-import pytz as _pytz
-
-from babel import localtime
-
 missing = object()
 
 _T = TypeVar("_T")
@@ -255,8 +253,8 @@ class FixedOffsetTimezone(tzinfo):
 
 # Export the localtime functionality here because that's
 # where it was in the past.
-UTC = _pytz.utc
-LOCALTZ = localtime.LOCALTZ
+UTC = dates.UTC
+LOCALTZ = dates.LOCALTZ
 get_localzone = localtime.get_localzone
 
 STDOFFSET = localtime.STDOFFSET
index 44201877efb75c8f472f61e364b0bd4bc87bbb02..8b35091154034b428c3213756fe365cfd784821c 100644 (file)
@@ -276,11 +276,9 @@ class, which you need appropriate implementations for to actually use in your
 application. Babel includes a ``tzinfo`` implementation for UTC (Universal
 Time).
 
-Babel uses `pytz`_ for real timezone support which includes the
-definitions of practically all of the time-zones used on the world, as
-well as important functions for reliably converting from UTC to local
-time, and vice versa.  The module is generally wrapped for you so you can
-directly interface with it from within Babel:
+Babel uses either `zoneinfo`_ or `pytz`_ for timezone support.
+If pytz is installed, it is preferred over the standard library's zoneinfo.
+You can directly interface with either of these modules from within Babel:
 
 .. code-block:: pycon
 
@@ -294,9 +292,9 @@ directly interface with it from within Babel:
 The recommended approach to deal with different time-zones in a Python
 application is to always use UTC internally, and only convert from/to the users
 time-zone when accepting user input and displaying date/time data, respectively.
-You can use Babel together with ``pytz`` to apply a time-zone to any
-``datetime`` or ``time`` object for display, leaving the original information
-unchanged:
+You can use Babel together with ``zoneinfo`` or ``pytz`` to apply a time-zone
+to any ``datetime`` or ``time`` object for display, leaving the original
+information unchanged:
 
 .. code-block:: pycon
 
@@ -314,6 +312,9 @@ For many timezones it's also possible to ask for the next timezone
 transition.  This for instance is useful to answer the question “when do I
 have to move the clock forward next”:
 
+.. warning:: ``get_next_timezone_transition`` is deprecated and will be removed
+             in the next version of Babel
+
 .. code-block:: pycon
 
     >>> t = get_next_timezone_transition('Europe/Vienna', datetime(2011, 3, 2))
@@ -339,7 +340,7 @@ your operating system.  It's provided through the ``LOCALTZ`` constant:
     >>> get_timezone_name(LOCALTZ)
     u'Central European Time'
 
-.. _pytz: http://pytz.sourceforge.net/
+.. _pytz: https://pythonhosted.org/pytz/
 
 
 Localized Time-zone Names
@@ -370,8 +371,9 @@ display a list of time-zones to the user.
 .. code-block:: pycon
 
     >>> from datetime import datetime
+    >>> from babel.dates import _localize
 
-    >>> dt = tz.localize(datetime(2007, 8, 15))
+    >>> dt = _localize(tz, datetime(2007, 8, 15))
     >>> get_timezone_name(dt, locale=Locale.parse('de_DE'))
     u'Mitteleurop\xe4ische Sommerzeit'
     >>> get_timezone_name(tz, locale=Locale.parse('de_DE'))
index 1c4453d8e90ad8ef29c625bd1b469e45a19090e3..97a105b98c6ba71b47f782087f525b71ab0ac73d 100644 (file)
@@ -46,10 +46,9 @@ Unicode is a big deal in Babel.  Here is how the rules are set up:
 Dates and Timezones
 -------------------
 
-Generally all timezone support in Babel is based on pytz which it just
-depends on.  Babel should assume that timezone objects are pytz based
-because those are the only ones with an API that actually work correctly
-(due to the API problems with non UTC based timezones).
+Babel's timezone support relies on either ``pytz`` or ``zoneinfo``; if ``pytz``
+is installed, it is preferred over ``zoneinfo``.  Babel should assume that any
+timezone objects can be from either of these modules.
 
 Assumptions to make:
 
index 26fe23a470e4ae163f1604f0b1e5c512960acd60..8bf614cb6e8f283bb361c618534b5b95bb7f8674 100644 (file)
@@ -4,10 +4,14 @@ Installation
 ============
 
 Babel is distributed as a standard Python package fully set up with all
-the dependencies it needs.  It primarily depends on the excellent `pytz`_
-library for timezone handling.  To install it you can use ``pip``.
+the dependencies it needs.  On Python versions where the standard library
+`zoneinfo`_ module is not available, `pytz`_  needs to be installed for
+timezone support. If `pytz`_  is installed, it is preferred over the
+standard library `zoneinfo`_  module where possible.
 
-.. _pytz: http://pytz.sourceforge.net/
+.. _pytz: https://pythonhosted.org/pytz/
+
+.. _zoneinfo: https://docs.python.org/3/library/zoneinfo.html
 
 .. _virtualenv:
 
@@ -79,16 +83,15 @@ Get the git checkout in a new virtualenv and run in development mode::
     New python executable in venv/bin/python
     Installing distribute............done.
     $ . venv/bin/activate
-    $ pip install pytz
     $ python setup.py import_cldr
     $ pip install --editable .
     ...
     Finished processing dependencies for Babel
 
-Make sure to not forget about the ``pip install pytz`` and ``import_cldr`` steps
-because otherwise you will be missing the locale data.
+Make sure to not forget about the ``import_cldr`` step because otherwise
+you will be missing the locale data.
 The custom setup command will download the most appropriate CLDR release from the
-official website and convert it for Babel but will not work without ``pytz``.
+official website and convert it for Babel.
 
 This will pull also in the dependencies and activate the git head as the
 current version inside the virtualenv.  Then all you have to do is run
index 157f7c160e61ee574a5393b09e48156a201164fa..db63216e991c43360cc6e628c2af9e2f3ce6ff3a 100755 (executable)
--- a/setup.py
+++ b/setup.py
@@ -63,7 +63,8 @@ setup(
         # This version identifier is currently necessary as
         # pytz otherwise does not install on pip 1.4 or
         # higher.
-        'pytz>=2015.7',
+        # Python 3.9 and later include zoneinfo which replaces pytz
+        'pytz>=2015.7; python_version<"3.9"',
     ],
     cmdclass={'import_cldr': import_cldr},
     zip_safe=False,
index dc3ae346f10b0617b8a0644f4f03db8479fc859d..eb3aa89773c16d0d7a3012651a7bf4b8a3760b66 100644 (file)
@@ -1,8 +1,8 @@
 import datetime
 
 from babel import dates
-from babel.dates import get_timezone
 from babel.util import UTC
+from .test_dates import timezone_getter
 
 TEST_DT = datetime.datetime(2016, 1, 8, 11, 46, 15)
 TEST_TIME = TEST_DT.time()
@@ -31,10 +31,10 @@ def test_format_interval_no_difference():
     assert dates.format_interval(t1, t2, "yMd", fuzzy=False, locale="fi") == "8.1.2016"
 
 
-def test_format_interval_in_tz():
+def test_format_interval_in_tz(timezone_getter):
     t1 = TEST_DT.replace(tzinfo=UTC)
     t2 = t1 + datetime.timedelta(minutes=18)
-    hki_tz = get_timezone("Europe/Helsinki")
+    hki_tz = timezone_getter("Europe/Helsinki")
     assert dates.format_interval(t1, t2, "Hmv", tzinfo=hki_tz, locale="fi") == "13.46\u201314.04 aikavyöhyke: Suomi"
 
 
index b62f542cdd07c705a6fa59ac49488bc573a75bc6..9fd73148bd2f2efa668fc2d3469578d8a2bead16 100644 (file)
 
 import calendar
 from datetime import date, datetime, time, timedelta
-import unittest
 
 import freezegun
 import pytest
-import pytz
-from pytz import timezone
+
+# for tests it can be useful to have both zoneinfo and pytz available
+try:
+    import zoneinfo
+except ModuleNotFoundError:
+    try:
+        from backports import zoneinfo
+    except ImportError:
+        zoneinfo = None
+
+try:
+    import pytz
+except ModuleNotFoundError:
+    pytz = None
 
 from babel import dates, Locale
-from babel.dates import NO_INHERITANCE_MARKER
+from babel.dates import NO_INHERITANCE_MARKER, _localize, _get_tz_name, LOCALTZ
 from babel.util import FixedOffsetTimezone
 
 
-@pytest.fixture(params=["pytz.timezone", "zoneinfo.ZoneInfo"])
+@pytest.fixture(params=["pytz.timezone", "zoneinfo.ZoneInfo"], scope="package")
 def timezone_getter(request):
     if request.param == "pytz.timezone":
-        return timezone
+        if pytz:
+            return pytz.timezone
+        else:
+            pytest.skip("pytz not available")
     elif request.param == "zoneinfo.ZoneInfo":
-        try:
-            import zoneinfo
-        except ImportError:
-            try:
-                from backports import zoneinfo
-            except ImportError:
-                pytest.skip("zoneinfo not available")
-        return zoneinfo.ZoneInfo
+        if zoneinfo:
+            return zoneinfo.ZoneInfo
+        else:
+            pytest.skip("zoneinfo not available")
     else:
         raise NotImplementedError
 
 
-class DateTimeFormatTestCase(unittest.TestCase):
+class DateTimeFormatTestCase:
 
     def test_quarter_format(self):
         d = date(2006, 6, 8)
@@ -205,33 +215,33 @@ class DateTimeFormatTestCase(unittest.TestCase):
         d = time(0, 0, 0)
         assert dates.DateTimeFormat(d, locale='en_US')['AAAA'] == '0000'
 
-    def test_timezone_rfc822(self):
-        tz = timezone('Europe/Berlin')
-        t = tz.localize(datetime(2015, 1, 1, 15, 30))
+    def test_timezone_rfc822(self, timezone_getter):
+        tz = timezone_getter('Europe/Berlin')
+        t = _localize(tz, datetime(2015, 1, 1, 15, 30))
         assert dates.DateTimeFormat(t, locale='de_DE')['Z'] == '+0100'
 
-    def test_timezone_gmt(self):
-        tz = timezone('Europe/Berlin')
-        t = tz.localize(datetime(2015, 1, 1, 15, 30))
+    def test_timezone_gmt(self, timezone_getter):
+        tz = timezone_getter('Europe/Berlin')
+        t = _localize(tz, datetime(2015, 1, 1, 15, 30))
         assert dates.DateTimeFormat(t, locale='de_DE')['ZZZZ'] == 'GMT+01:00'
 
-    def test_timezone_name(self):
-        tz = timezone('Europe/Paris')
-        dt = tz.localize(datetime(2007, 4, 1, 15, 30))
+    def test_timezone_name(self, timezone_getter):
+        tz = timezone_getter('Europe/Paris')
+        dt = _localize(tz, datetime(2007, 4, 1, 15, 30))
         assert dates.DateTimeFormat(dt, locale='fr_FR')['v'] == 'heure : France'
 
-    def test_timezone_location_format(self):
-        tz = timezone('Europe/Paris')
-        dt = datetime(2007, 4, 1, 15, 30, tzinfo=tz)
+    def test_timezone_location_format(self, timezone_getter):
+        tz = timezone_getter('Europe/Paris')
+        dt = _localize(tz, datetime(2007, 4, 1, 15, 30))
         assert dates.DateTimeFormat(dt, locale='fr_FR')['VVVV'] == 'heure : France'
 
-    def test_timezone_walltime_short(self):
-        tz = timezone('Europe/Paris')
+    def test_timezone_walltime_short(self, timezone_getter):
+        tz = timezone_getter('Europe/Paris')
         t = time(15, 30, tzinfo=tz)
         assert dates.DateTimeFormat(t, locale='fr_FR')['v'] == 'heure : France'
 
-    def test_timezone_walltime_long(self):
-        tz = timezone('Europe/Paris')
+    def test_timezone_walltime_long(self, timezone_getter):
+        tz = timezone_getter('Europe/Paris')
         t = time(15, 30, tzinfo=tz)
         assert dates.DateTimeFormat(t, locale='fr_FR')['vvvv'] == u'heure d’Europe centrale'
 
@@ -249,7 +259,7 @@ class DateTimeFormatTestCase(unittest.TestCase):
         assert dates.format_time(t, 'K a', locale=l) == '0 PM'
 
 
-class FormatDateTestCase(unittest.TestCase):
+class FormatDateTestCase:
 
     def test_with_time_fields_in_pattern(self):
         with pytest.raises(AttributeError):
@@ -265,18 +275,18 @@ class FormatDateTestCase(unittest.TestCase):
         assert dates.format_date(d, 'w', locale='en_US') == '14'
 
 
-class FormatDatetimeTestCase(unittest.TestCase):
+class FormatDatetimeTestCase:
 
-    def test_with_float(self):
-        d = datetime(2012, 4, 1, 15, 30, 29, tzinfo=timezone('UTC'))
+    def test_with_float(self, timezone_getter):
+        UTC = timezone_getter('UTC')
+        d = datetime(2012, 4, 1, 15, 30, 29, tzinfo=UTC)
         epoch = float(calendar.timegm(d.timetuple()))
         formatted_string = dates.format_datetime(epoch, format='long', locale='en_US')
         assert formatted_string == u'April 1, 2012 at 3:30:29 PM UTC'
 
-    def test_timezone_formats_los_angeles(self):
-        dt = datetime(2016, 1, 13, 7, 8, 35)
-        tz = dates.get_timezone('America/Los_Angeles')
-        dt = tz.localize(dt)
+    def test_timezone_formats_los_angeles(self, timezone_getter):
+        tz = timezone_getter('America/Los_Angeles')
+        dt = _localize(tz, datetime(2016, 1, 13, 7, 8, 35))
         assert dates.format_datetime(dt, 'z', locale='en') == u'PST'
         assert dates.format_datetime(dt, 'zz', locale='en') == u'PST'
         assert dates.format_datetime(dt, 'zzz', locale='en') == u'PST'
@@ -300,10 +310,9 @@ class FormatDatetimeTestCase(unittest.TestCase):
         assert dates.format_datetime(dt, 'xxxx', locale='en') == u'-0800'
         assert dates.format_datetime(dt, 'xxxxx', locale='en') == u'-08:00'
 
-    def test_timezone_formats_utc(self):
-        dt = datetime(2016, 1, 13, 7, 8, 35)
-        tz = dates.get_timezone('UTC')
-        dt = tz.localize(dt)
+    def test_timezone_formats_utc(self, timezone_getter):
+        tz = timezone_getter('UTC')
+        dt = _localize(tz, datetime(2016, 1, 13, 7, 8, 35))
         assert dates.format_datetime(dt, 'Z', locale='en') == u'+0000'
         assert dates.format_datetime(dt, 'ZZ', locale='en') == u'+0000'
         assert dates.format_datetime(dt, 'ZZZ', locale='en') == u'+0000'
@@ -323,10 +332,9 @@ class FormatDatetimeTestCase(unittest.TestCase):
         assert dates.format_datetime(dt, 'xxxx', locale='en') == u'+0000'
         assert dates.format_datetime(dt, 'xxxxx', locale='en') == u'+00:00'
 
-    def test_timezone_formats_kolkata(self):
-        dt = datetime(2016, 1, 13, 7, 8, 35)
-        tz = dates.get_timezone('Asia/Kolkata')
-        dt = tz.localize(dt)
+    def test_timezone_formats_kolkata(self, timezone_getter):
+        tz = timezone_getter('Asia/Kolkata')
+        dt = _localize(tz, datetime(2016, 1, 13, 7, 8, 35))
         assert dates.format_datetime(dt, 'zzzz', locale='en') == u'India Standard Time'
         assert dates.format_datetime(dt, 'ZZZZ', locale='en') == u'GMT+05:30'
         assert dates.format_datetime(dt, 'ZZZZZ', locale='en') == u'+05:30'
@@ -345,18 +353,19 @@ class FormatDatetimeTestCase(unittest.TestCase):
         assert dates.format_datetime(dt, 'xxxxx', locale='en') == u'+05:30'
 
 
-class FormatTimeTestCase(unittest.TestCase):
+class FormatTimeTestCase:
 
-    def test_with_naive_datetime_and_tzinfo(self):
+    def test_with_naive_datetime_and_tzinfo(self, timezone_getter):
         assert dates.format_time(
             datetime(2007, 4, 1, 15, 30),
             'long',
-            tzinfo=timezone('US/Eastern'),
+            tzinfo=timezone_getter('US/Eastern'),
             locale='en',
         ) == '11:30:00 AM EDT'
 
-    def test_with_float(self):
-        d = datetime(2012, 4, 1, 15, 30, 29, tzinfo=timezone('UTC'))
+    def test_with_float(self, timezone_getter):
+        tz = timezone_getter('UTC')
+        d = _localize(tz, datetime(2012, 4, 1, 15, 30, 29))
         epoch = float(calendar.timegm(d.timetuple()))
         assert dates.format_time(epoch, format='long', locale='en_US') == u'3:30:29 PM UTC'
 
@@ -369,7 +378,7 @@ class FormatTimeTestCase(unittest.TestCase):
             dates.format_time(datetime(2007, 4, 1, 15, 30), "yyyy-MM-dd HH:mm", locale='en_US')
 
 
-class FormatTimedeltaTestCase(unittest.TestCase):
+class FormatTimedeltaTestCase:
 
     def test_zero_seconds(self):
         td = timedelta(seconds=0)
@@ -398,7 +407,7 @@ class FormatTimedeltaTestCase(unittest.TestCase):
                 dates.format_timedelta(timedelta(hours=1), format=format)
 
 
-class TimeZoneAdjustTestCase(unittest.TestCase):
+class TimeZoneAdjustTestCase:
 
     def _utc(self):
         class EvilFixedOffsetTimezone(FixedOffsetTimezone):
@@ -410,7 +419,7 @@ class TimeZoneAdjustTestCase(unittest.TestCase):
         assert hasattr(UTC, 'normalize') is False
         return UTC
 
-    def test_can_format_time_with_non_pytz_timezone(self):
+    def test_can_format_time_with_custom_timezone(self):
         # regression test for #257
         utc = self._utc()
         t = datetime(2007, 4, 1, 15, 30, tzinfo=utc)
@@ -465,13 +474,13 @@ def test_get_time_format():
             u'HH:mm:ss zzzz')
 
 
-def test_get_timezone_gmt():
+def test_get_timezone_gmt(timezone_getter):
     dt = datetime(2007, 4, 1, 15, 30)
     assert dates.get_timezone_gmt(dt, locale='en') == u'GMT+00:00'
     assert dates.get_timezone_gmt(dt, locale='en', return_z=True) == 'Z'
     assert dates.get_timezone_gmt(dt, locale='en', width='iso8601_short') == u'+00'
-    tz = timezone('America/Los_Angeles')
-    dt = tz.localize(datetime(2007, 4, 1, 15, 30))
+    tz = timezone_getter('America/Los_Angeles')
+    dt = _localize(tz, datetime(2007, 4, 1, 15, 30))
     assert dates.get_timezone_gmt(dt, locale='en') == u'GMT-07:00'
     assert dates.get_timezone_gmt(dt, 'short', locale='en') == u'-0700'
     assert dates.get_timezone_gmt(dt, locale='en', width='iso8601_short') == u'-07'
@@ -582,21 +591,27 @@ def test_format_date():
             u"Sun, Apr 1, '07")
 
 
-def test_format_datetime():
+def test_format_datetime(timezone_getter):
     dt = datetime(2007, 4, 1, 15, 30)
     assert (dates.format_datetime(dt, locale='en_US') ==
             u'Apr 1, 2007, 3:30:00 PM')
 
-    full = dates.format_datetime(dt, 'full', tzinfo=timezone('Europe/Paris'),
-                                 locale='fr_FR')
+    full = dates.format_datetime(
+        dt, 'full',
+        tzinfo=timezone_getter('Europe/Paris'),
+        locale='fr_FR'
+    )
     assert full == (u'dimanche 1 avril 2007 à 17:30:00 heure '
                     u'd\u2019\xe9t\xe9 d\u2019Europe centrale')
-    custom = dates.format_datetime(dt, "yyyy.MM.dd G 'at' HH:mm:ss zzz",
-                                   tzinfo=timezone('US/Eastern'), locale='en')
+    custom = dates.format_datetime(
+        dt, "yyyy.MM.dd G 'at' HH:mm:ss zzz",
+        tzinfo=timezone_getter('US/Eastern'),
+        locale='en'
+    )
     assert custom == u'2007.04.01 AD at 11:30:00 EDT'
 
 
-def test_format_time():
+def test_format_time(timezone_getter):
     t = time(15, 30)
     assert dates.format_time(t, locale='en_US') == u'3:30:00 PM'
     assert dates.format_time(t, format='short', locale='de_DE') == u'15:30'
@@ -604,31 +619,31 @@ def test_format_time():
     assert (dates.format_time(t, "hh 'o''clock' a", locale='en') ==
             u"03 o'clock PM")
 
-    t = datetime(2007, 4, 1, 15, 30)
-    tzinfo = timezone('Europe/Paris')
-    t = tzinfo.localize(t)
-    fr = dates.format_time(t, format='full', tzinfo=tzinfo, locale='fr_FR')
-    assert fr == u'15:30:00 heure d\u2019\xe9t\xe9 d\u2019Europe centrale'
-    custom = dates.format_time(t, "hh 'o''clock' a, zzzz",
-                               tzinfo=timezone('US/Eastern'), locale='en')
+    paris = timezone_getter('Europe/Paris')
+    eastern = timezone_getter('US/Eastern')
+
+    t = _localize(paris, datetime(2007, 4, 1, 15, 30))
+    fr = dates.format_time(t, format='full', tzinfo=paris, locale='fr_FR')
+    assert fr == '15:30:00 heure d’été d’Europe centrale'
+
+    custom = dates.format_time(t, "hh 'o''clock' a, zzzz", tzinfo=eastern, locale='en')
     assert custom == u"09 o'clock AM, Eastern Daylight Time"
 
     t = time(15, 30)
-    paris = dates.format_time(t, format='full',
-                              tzinfo=timezone('Europe/Paris'), locale='fr_FR')
-    assert paris == u'15:30:00 heure normale d\u2019Europe centrale'
-    us_east = dates.format_time(t, format='full',
-                                tzinfo=timezone('US/Eastern'), locale='en_US')
+    paris = dates.format_time(t, format='full', tzinfo=paris, locale='fr_FR')
+    assert paris == '15:30:00 heure normale d’Europe centrale'
+
+    us_east = dates.format_time(t, format='full', tzinfo=eastern, locale='en_US')
     assert us_east == u'3:30:00 PM Eastern Standard Time'
 
 
-def test_format_skeleton():
+def test_format_skeleton(timezone_getter):
     dt = datetime(2007, 4, 1, 15, 30)
     assert (dates.format_skeleton('yMEd', dt, locale='en_US') == u'Sun, 4/1/2007')
     assert (dates.format_skeleton('yMEd', dt, locale='th') == u'อา. 1/4/2007')
 
     assert (dates.format_skeleton('EHm', dt, locale='en') == u'Sun 15:30')
-    assert (dates.format_skeleton('EHm', dt, tzinfo=timezone('Asia/Bangkok'), locale='th') == u'อา. 22:30 น.')
+    assert (dates.format_skeleton('EHm', dt, tzinfo=timezone_getter('Asia/Bangkok'), locale='th') == u'อา. 22:30 น.')
 
 
 def test_format_timedelta():
@@ -721,18 +736,18 @@ def test_format_current_moment():
 
 
 @pytest.mark.all_locales
-def test_no_inherit_metazone_marker_never_in_output(locale):
+def test_no_inherit_metazone_marker_never_in_output(locale, timezone_getter):
     # See: https://github.com/python-babel/babel/issues/428
-    tz = pytz.timezone('America/Los_Angeles')
-    t = tz.localize(datetime(2016, 1, 6, 7))
+    tz = timezone_getter('America/Los_Angeles')
+    t = _localize(tz, datetime(2016, 1, 6, 7))
     assert NO_INHERITANCE_MARKER not in dates.format_time(t, format='long', locale=locale)
     assert NO_INHERITANCE_MARKER not in dates.get_timezone_name(t, width='short', locale=locale)
 
 
-def test_no_inherit_metazone_formatting():
+def test_no_inherit_metazone_formatting(timezone_getter):
     # See: https://github.com/python-babel/babel/issues/428
-    tz = pytz.timezone('America/Los_Angeles')
-    t = tz.localize(datetime(2016, 1, 6, 7))
+    tz = timezone_getter('America/Los_Angeles')
+    t = _localize(tz, datetime(2016, 1, 6, 7))
     assert dates.format_time(t, format='long', locale='en_US') == "7:00:00 AM PST"
     assert dates.format_time(t, format='long', locale='en_GB') == "07:00:00 Pacific Standard Time"
     assert dates.get_timezone_name(t, width='short', locale='en_US') == "PST"
index c73e53ba7b84438eea79a4b1b3cc7304ba09961e..d39d8ef6d4b84782a7c29c07a4fa053e83f5634c 100644 (file)
@@ -23,6 +23,8 @@ from io import BytesIO
 from babel import support
 from babel.messages import Catalog
 from babel.messages.mofile import write_mo
+from babel.dates import get_timezone
+from .test_dates import timezone_getter
 
 
 SKIP_LGETTEXT = sys.version_info >= (3, 8)
@@ -300,16 +302,14 @@ def test_format_date():
     assert fmt.date(date(2007, 4, 1)) == 'Apr 1, 2007'
 
 
-def test_format_datetime():
-    from pytz import timezone
-    fmt = support.Format('en_US', tzinfo=timezone('US/Eastern'))
+def test_format_datetime(timezone_getter):
+    fmt = support.Format('en_US', tzinfo=timezone_getter('US/Eastern'))
     when = datetime(2007, 4, 1, 15, 30)
     assert fmt.datetime(when) == 'Apr 1, 2007, 11:30:00 AM'
 
 
-def test_format_time():
-    from pytz import timezone
-    fmt = support.Format('en_US', tzinfo=timezone('US/Eastern'))
+def test_format_time(timezone_getter):
+    fmt = support.Format('en_US', tzinfo=timezone_getter('US/Eastern'))
     assert fmt.time(datetime(2007, 4, 1, 15, 30)) == '11:30:00 AM'