Repository: python-babel/babel
Pull request #1203 (2025-03-20 07:53:44)

Use standard library `timezone` instead of `FixedOffsetTimezone`
Noticed `FixedOffsetTimezone`s didn't unpickle, and then realized we shouldn't use them at all.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/python-babel/babel/pull/1203?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 90.84%. Comparing base [(`2e56a2a`)](https://app.codecov.io/gh/python-babel/babel/commit/2e56a2adef4152f0eea2fd7964452c60f792dcc7?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) to head [(`77da5a3`)](https://app.codecov.io/gh/python-babel/babel/commit/77da5a39d06944832118593c45e05798f3747422?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #1203      +/-   ##
==========================================
- Coverage   91.71%   90.84%   -0.88%     
==========================================
  Files          27       27              
  Lines        4696     4696              
==========================================
- Hits         4307     4266      -41     
- Misses        389      430      +41     
```

| [Flag](https://app.codecov.io/gh/python-babel/babel/pull/1203/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | Coverage Δ | |
|---|---|---|
| [macos-14-3.10](https://app.codecov.io/gh/python-babel/babel/pull/1203/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `90.73% <100.00%> (-0.03%)` | :arrow_down: |
| [macos-14-3.11](https://app.codecov.io/gh/python-babel/babel/pull/1203/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `?` | |
| [macos-14-3.12](https://app.codecov.io/gh/python-babel/babel/pull/1203/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `?` | |
| [macos-14-3.13](https://app.codecov.io/gh/python-babel/babel/pull/1203/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `?` | |
| [macos-14-3.8](https://app.codecov.io/gh/python-babel/babel/pull/1203/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `?` | |
| [macos-14-3.9](https://app.codecov.io/gh/python-babel/babel/pull/1203/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `?` | |
| [macos-14-pypy3.10](https://app.codecov.io/gh/python-babel/babel/pull/1203/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `?` | |
| [ubuntu-24.04-3.10](https://app.codecov.io/gh/python-babel/babel/pull/1203/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `90.75% <100.00%> (-0.03%)` | :arrow_down: |
| [ubuntu-24.04-3.11](https://app.codecov.io/gh/python-babel/babel/pull/1203/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `?` | |
| [ubuntu-24.04-3.12](https://app.codecov.io/gh/python-babel/babel/pull/1203/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `?` | |
| [ubuntu-24.04-3.13](https://app.codecov.io/gh/python-babel/babel/pull/1203/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `?` | |
| [ubuntu-24.04-3.8](https://app.codecov.io/gh/python-babel/babel/pull/1203/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `?` | |
| [ubuntu-24.04-3.9](https://app.codecov.io/gh/python-babel/babel/pull/1203/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `90.68% <100.00%> (-0.03%)` | :arrow_down: |
| [ubuntu-24.04-pypy3.10](https://app.codecov.io/gh/python-babel/babel/pull/1203/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `?` | |
| [windows-2022-3.10](https://app.codecov.io/gh/python-babel/babel/pull/1203/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `?` | |
| [windows-2022-3.11](https://app.codecov.io/gh/python-babel/babel/pull/1203/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `?` | |
| [windows-2022-3.12](https://app.codecov.io/gh/python-babel/babel/pull/1203/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `?` | |
| [windows-2022-3.13](https://app.codecov.io/gh/python-babel/babel/pull/1203/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `?` | |
| [windows-2022-3.8](https://app.codecov.io/gh/python-babel/babel/pull/1203/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `?` | |
| [windows-2022-3.9](https://app.codecov.io/gh/python-babel/babel/pull/1203/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `?` | |
| [windows-2022-pypy3.10](https://app.codecov.io/gh/python-babel/babel/pull/1203/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `?` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/python-babel/babel/pull/1203?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel).

<details><summary>🚀 New features to boost your workflow: </summary>

- ❄ [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- 📦 [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>

Patch:
diff --git a/babel/messages/catalog.py b/babel/messages/catalog.py
index a35647f..de96ea5 100644
--- a/babel/messages/catalog.py
+++ b/babel/messages/catalog.py
@@ -23,7 +23,7 @@ from babel import __version__ as VERSION
 from babel.core import Locale, UnknownLocaleError
 from babel.dates import format_datetime
 from babel.messages.plurals import get_plural
-from babel.util import LOCALTZ, FixedOffsetTimezone, _cmp, distinct
+from babel.util import LOCALTZ, _cmp, distinct
 
 if TYPE_CHECKING:
     from typing_extensions import TypeAlias
@@ -118,7 +118,10 @@ def _parse_datetime_header(value: str) -> datetime.datetime:
         net_mins_offset *= plus_minus
 
         # Create an offset object
-        tzoffset = FixedOffsetTimezone(net_mins_offset)
+        tzoffset = datetime.timezone(
+            offset=datetime.timedelta(minutes=net_mins_offset),
+            name=f'Etc/GMT{net_mins_offset:+d}',
+        )
 
         # Store the offset in a datetime object
         dt = dt.replace(tzinfo=tzoffset)
diff --git a/babel/util.py b/babel/util.py
index d113982..c91c12b 100644
--- a/babel/util.py
+++ b/babel/util.py
@@ -255,10 +255,20 @@ odict = dict
 
 
 class FixedOffsetTimezone(datetime.tzinfo):
-    """Fixed offset in minutes east from UTC."""
+    """
+    Fixed offset in minutes east from UTC.
 
-    def __init__(self, offset: float, name: str | None = None) -> None:
+    DEPRECATED: Use the standard library `datetime.timezone` instead.
+    """
+    # TODO (Babel 3.x): Remove this class
 
+    def __init__(self, offset: float, name: str | None = None) -> None:
+        warnings.warn(
+            "`FixedOffsetTimezone` is deprecated and will be removed in a future version of Babel. "
+            "Use the standard library `datetime.timezone` class.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
         self._offset = datetime.timedelta(minutes=offset)
         if name is None:
             name = 'Etc/GMT%+d' % offset
diff --git a/pyproject.toml b/pyproject.toml
index 98ab326..814f0b1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -50,5 +50,7 @@ markers = [
 ]
 filterwarnings = [
     # The doctest for format_number would raise this, but we don't really want to see it.
-    "ignore:babel.numbers.format_decimal:DeprecationWarning"
+    "ignore:babel.numbers.format_decimal:DeprecationWarning",
+    # FixedOffsetTimezone is still being tested, but we don't want to see the deprecation warning.
+    "ignore:.*FixedOffsetTimezone:DeprecationWarning",
 ]


Test patch:
diff --git a/tests/messages/test_catalog.py b/tests/messages/test_catalog.py
index 692931e..7fdc0ba 100644
--- a/tests/messages/test_catalog.py
+++ b/tests/messages/test_catalog.py
@@ -12,6 +12,7 @@
 
 import copy
 import datetime
+import pickle
 import unittest
 from io import StringIO
 
@@ -524,10 +525,10 @@ def test_catalog_update():
 
 def test_datetime_parsing():
     val1 = catalog._parse_datetime_header('2006-06-28 23:24+0200')
-    assert val1.year == 2006
-    assert val1.month == 6
-    assert val1.day == 28
-    assert val1.tzinfo.zone == 'Etc/GMT+120'
+    assert val1.timetuple()[:5] == (2006, 6, 28, 23, 24)
+    assert val1.utctimetuple()[:5] == (2006, 6, 28, 21, 24)
+    assert val1.tzinfo.tzname(None) == 'Etc/GMT+120'
+    assert val1 == datetime.datetime(2006, 6, 28, 21, 24, tzinfo=UTC)
 
     val2 = catalog._parse_datetime_header('2006-06-28 23:24')
     assert val2.year == 2006
@@ -562,3 +563,16 @@ def test_update_catalog_comments():
 
     # Auto comments will be obliterated here
     assert all(message.user_comments for message in catalog if message.id)
+
+
+def test_catalog_tz_pickleable():
+    """
+    Test that catalogs with timezoned times are pickleable.
+    This would previously fail with `FixedOffsetTimezone.__init__() missing 1 required positional argument: 'offset'`
+    when trying to load the pickled data.
+    """
+    pickle.loads(pickle.dumps(pofile.read_po(StringIO(r"""
+msgid ""
+msgstr ""
+"POT-Creation-Date: 2007-04-01 15:30+0200\n"
+    """))))