]> git.ipfire.org Git - thirdparty/sqlalchemy/alembic.git/commitdiff
Use zoneinfo instead of dateutil.
authorFederico Caselli <cfederico87@gmail.com>
Thu, 26 Oct 2023 22:10:01 +0000 (00:10 +0200)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sat, 18 Nov 2023 14:38:01 +0000 (09:38 -0500)
Replaced ``python-dateutil`` with the standard library module
`zoneinfo <https://docs.python.org/3.11/library/zoneinfo.html#module-zoneinfo>`.
This module was added in Python 3.9, so previous version will been
to install the backport of it, available by installing the ``backports.zoneinfo``
library. The ``alembic[tz]`` option has been updated accordingly.

Fixes: #1339
Change-Id: I935fcca52ae0d3b80dacbf05137ab57ee54d054c

alembic/script/base.py
alembic/templates/async/alembic.ini.mako
alembic/templates/generic/alembic.ini.mako
alembic/templates/multidb/alembic.ini.mako
docs/build/requirements.txt
docs/build/tutorial.rst
docs/build/unreleased/1339.rst [new file with mode: 0644]
setup.cfg
tests/test_script_production.py
tox.ini

index d0f9abbde4be082cfd25ae2a2a418c04181997d7..5766d838721b8d783b9a6cd010a65d133fa3b0de 100644 (file)
@@ -23,6 +23,7 @@ from . import revision
 from . import write_hooks
 from .. import util
 from ..runtime import migration
+from ..util import compat
 from ..util import not_none
 
 if TYPE_CHECKING:
@@ -35,9 +36,14 @@ if TYPE_CHECKING:
     from ..runtime.migration import StampStep
 
 try:
-    from dateutil import tz
+    if compat.py39:
+        from zoneinfo import ZoneInfo
+        from zoneinfo import ZoneInfoNotFoundError
+    else:
+        from backports.zoneinfo import ZoneInfo  # type: ignore[import-not-found,no-redef] # noqa: E501
+        from backports.zoneinfo import ZoneInfoNotFoundError  # type: ignore[import-not-found,no-redef] # noqa: E501
 except ImportError:
-    tz = None  # type: ignore[assignment]
+    ZoneInfo = None  # type: ignore[assignment, misc]
 
 _sourceless_rev_file = re.compile(r"(?!\.\#|__init__)(.*\.py)(c|o)?$")
 _only_source_rev_file = re.compile(r"(?!\.\#|__init__)(.*\.py)$")
@@ -604,23 +610,26 @@ class ScriptDirectory:
 
     def _generate_create_date(self) -> datetime.datetime:
         if self.timezone is not None:
-            if tz is None:
+            if ZoneInfo is None:
                 raise util.CommandError(
-                    "The library 'python-dateutil' is required "
-                    "for timezone support"
+                    "Python >= 3.9 is required for timezone support or"
+                    "the 'backports.zoneinfo' package must be installed."
                 )
             # First, assume correct capitalization
-            tzinfo = tz.gettz(self.timezone)
-            if tzinfo is None:
-                # Fall back to uppercase
-                tzinfo = tz.gettz(self.timezone.upper())
+            try:
+                tzinfo = ZoneInfo(self.timezone)
+            except ZoneInfoNotFoundError:
+                tzinfo = None
             if tzinfo is None:
-                raise util.CommandError(
-                    "Can't locate timezone: %s" % self.timezone
-                )
+                try:
+                    tzinfo = ZoneInfo(self.timezone.upper())
+                except ZoneInfoNotFoundError:
+                    raise util.CommandError(
+                        "Can't locate timezone: %s" % self.timezone
+                    ) from None
             create_date = (
                 datetime.datetime.utcnow()
-                .replace(tzinfo=tz.tzutc())
+                .replace(tzinfo=datetime.timezone.utc)
                 .astimezone(tzinfo)
             )
         else:
index bc9f2d50ff3e6d0f47059ca09d3e25b4e5244d77..c9a06d44f252745883289b902dd3653744192137 100644 (file)
@@ -14,9 +14,9 @@ prepend_sys_path = .
 
 # timezone to use when rendering the date within the migration file
 # as well as the filename.
-# If specified, requires the python-dateutil library that can be
-# installed by adding `alembic[tz]` to the pip requirements
-# string value is passed to dateutil.tz.gettz()
+# If specified, requires the python>=3.9 or backports.zoneinfo library.
+# Any requied deps can installed by adding `alembic[tz]` to the pip requirements
+# string value is passed to ZoneInfo()
 # leave blank for localtime
 # timezone =
 
index c18ddb4e0477aeda4935c254a5c19bcb5013b518..7dfaae5a4831dc4937947c150967e1de7bf9f26a 100644 (file)
@@ -16,9 +16,9 @@ prepend_sys_path = .
 
 # timezone to use when rendering the date within the migration file
 # as well as the filename.
-# If specified, requires the python-dateutil library that can be
-# installed by adding `alembic[tz]` to the pip requirements
-# string value is passed to dateutil.tz.gettz()
+# If specified, requires the python>=3.9 or backports.zoneinfo library.
+# Any requied deps can installed by adding `alembic[tz]` to the pip requirements
+# string value is passed to ZoneInfo()
 # leave blank for localtime
 # timezone =
 
index a9ea075516b474c9a55ea2a674db6e8abd3a75ce..f300bc8a025235ecc744d044b5a750ed4c3798f9 100644 (file)
@@ -16,9 +16,9 @@ prepend_sys_path = .
 
 # timezone to use when rendering the date within the migration file
 # as well as the filename.
-# If specified, requires the python-dateutil library that can be
-# installed by adding `alembic[tz]` to the pip requirements
-# string value is passed to dateutil.tz.gettz()
+# If specified, requires the python>=3.9 or backports.zoneinfo library.
+# Any requied deps can installed by adding `alembic[tz]` to the pip requirements
+# string value is passed to ZoneInfo()
 # leave blank for localtime
 # timezone =
 
index d042a565be699e69b8f4b4f32405a466e6fd2743..bf162865ee5dba83c7fec1b9e01c4ec52ae8be0c 100644 (file)
@@ -1,7 +1,7 @@
 git+https://github.com/sqlalchemyorg/changelog.git#egg=changelog
 git+https://github.com/sqlalchemyorg/sphinx-paramlinks.git#egg=sphinx-paramlinks
 git+https://github.com/sqlalchemy/sqlalchemy.git
-python-dateutil
+backports.zoneinfo;python_version<"3.9"
 # because there's a dependency in pyfiles.py
 Mako
 importlib-metadata;python_version<"3.9"
index 6e9552c5883d00a0b1b89a3b242aaa954dd7b12f..1a66528a912bcfa14d03f22638747b10960f728a 100644 (file)
@@ -141,9 +141,9 @@ The file generated with the "generic" configuration looks like::
 
     # timezone to use when rendering the date within the migration file
     # as well as the filename.
-    # If specified, requires the python-dateutil library that can be
-    # installed by adding `alembic[tz]` to the pip requirements
-    # string value is passed to dateutil.tz.gettz()
+    # If specified, requires the python>=3.9 or backports.zoneinfo library.
+    # Any requied deps can installed by adding `alembic[tz]` to the pip requirements
+    # string value is passed to ZoneInfo()
     # leave blank for localtime
     # timezone =
 
@@ -297,16 +297,18 @@ This file contains the following features:
 
 * ``timezone`` - an optional timezone name (e.g. ``UTC``, ``EST5EDT``, etc.)
   that will be applied to the timestamp which renders inside the migration
-  file's comment as well as within the filename. This option requires installing
-  the ``python-dateutil`` library. If ``timezone`` is specified,
+  file's comment as well as within the filename. This option requires Python>=3.9
+  or installing the ``backports.zoneinfo`` library. If ``timezone`` is specified,
   the create date object is no longer derived from ``datetime.datetime.now()``
   and is instead generated as::
 
       datetime.datetime.utcnow().replace(
-            tzinfo=dateutil.tz.tzutc()
-      ).astimezone(
-          dateutil.tz.gettz(<timezone>)
-      )
+        tzinfo=datetime.timezone.utc
+      ).astimezone(ZoneInfo(<timezone>))
+
+  .. versionchanged:: 1.13.0 Python standard library ``zoneinfo`` is now used
+     for timezone rendering in migrations; previously ``python-dateutil``
+     was used.
 
 * ``truncate_slug_length`` - defaults to 40, the max number of characters
   to include in the "slug" field.
diff --git a/docs/build/unreleased/1339.rst b/docs/build/unreleased/1339.rst
new file mode 100644 (file)
index 0000000..9f4560c
--- /dev/null
@@ -0,0 +1,9 @@
+.. change::
+    :tags: usecase
+    :tickets: 1339
+
+    Replaced ``python-dateutil`` with the standard library module
+    `zoneinfo <https://docs.python.org/3.11/library/zoneinfo.html#module-zoneinfo>`.
+    This module was added in Python 3.9, so previous version will been
+    to install the backport of it, available by installing the ``backports.zoneinfo``
+    library. The ``alembic[tz]`` option has been updated accordingly.
index 7453183c6e15cd0e84cac80313d95c8d166f320d..6654c46280529e7814fcabdf2abe5a0b7d3796a3 100644 (file)
--- a/setup.cfg
+++ b/setup.cfg
@@ -48,7 +48,7 @@ install_requires =
 
 [options.extras_require]
 tz =
-    python-dateutil
+    backports.zoneinfo;python_version<"3.9"
 
 [options.package_data]
 alembic = *.pyi, py.typed
index ccc1a107d3b4fda0bd755e0b709725814a47c938..3b5a6f60461eb7905c28b52c58bff7f31b36fe2a 100644 (file)
@@ -4,7 +4,6 @@ from pathlib import Path
 import re
 from unittest.mock import patch
 
-from dateutil import tz
 import sqlalchemy as sa
 from sqlalchemy import Column
 from sqlalchemy import inspect
@@ -41,6 +40,11 @@ from alembic.testing.env import write_script
 from alembic.testing.fixtures import TestBase
 from alembic.util import CommandError
 
+try:
+    from zoneinfo import ZoneInfo
+except ImportError:
+    from backports.zoneinfo import ZoneInfo
+
 env, abc, def_ = None, None, None
 
 
@@ -189,12 +193,16 @@ class ScriptNamingTest(TestBase):
 
     @testing.combinations(
         (
-            datetime.datetime(2012, 7, 25, 15, 8, 5, tzinfo=tz.gettz("UTC")),
+            datetime.datetime(
+                2012, 7, 25, 15, 8, 5, tzinfo=datetime.timezone.utc
+            ),
             "%s/versions/1343228885_12345_this_is_a_"
             "message_2012_7_25_15_8_5.py",
         ),
         (
-            datetime.datetime(2012, 7, 25, 15, 8, 6, tzinfo=tz.gettz("UTC")),
+            datetime.datetime(
+                2012, 7, 25, 15, 8, 6, tzinfo=datetime.timezone.utc
+            ),
             "%s/versions/1343228886_12345_this_is_a_"
             "message_2012_7_25_15_8_6.py",
         ),
@@ -227,7 +235,8 @@ class ScriptNamingTest(TestBase):
         with mock.patch(
             "alembic.script.base.datetime",
             mock.Mock(
-                datetime=mock.Mock(utcnow=lambda: given, now=lambda: given)
+                datetime=mock.Mock(utcnow=lambda: given, now=lambda: given),
+                timezone=datetime.timezone,
             ),
         ):
             create_date = script._generate_create_date()
@@ -238,7 +247,7 @@ class ScriptNamingTest(TestBase):
             "EST5EDT",
             datetime.datetime(2012, 7, 25, 15, 8, 5),
             datetime.datetime(
-                2012, 7, 25, 11, 8, 5, tzinfo=tz.gettz("EST5EDT")
+                2012, 7, 25, 11, 8, 5, tzinfo=ZoneInfo("EST5EDT")
             ),
         )
 
@@ -247,7 +256,7 @@ class ScriptNamingTest(TestBase):
             "est5edt",
             datetime.datetime(2012, 7, 25, 15, 8, 5),
             datetime.datetime(
-                2012, 7, 25, 11, 8, 5, tzinfo=tz.gettz("EST5EDT")
+                2012, 7, 25, 11, 8, 5, tzinfo=ZoneInfo("EST5EDT")
             ),
         )
 
@@ -255,7 +264,7 @@ class ScriptNamingTest(TestBase):
         self._test_tz(
             "utc",
             datetime.datetime(2012, 7, 25, 15, 8, 5),
-            datetime.datetime(2012, 7, 25, 15, 8, 5, tzinfo=tz.gettz("UTC")),
+            datetime.datetime(2012, 7, 25, 15, 8, 5, tzinfo=ZoneInfo("UTC")),
         )
 
     def test_custom_tzdata_tz(self):
@@ -263,7 +272,7 @@ class ScriptNamingTest(TestBase):
             "Europe/Berlin",
             datetime.datetime(2012, 7, 25, 15, 8, 5),
             datetime.datetime(
-                2012, 7, 25, 17, 8, 5, tzinfo=tz.gettz("Europe/Berlin")
+                2012, 7, 25, 17, 8, 5, tzinfo=ZoneInfo("Europe/Berlin")
             ),
         )
 
@@ -284,10 +293,12 @@ class ScriptNamingTest(TestBase):
             datetime.datetime(2012, 7, 25, 15, 8, 5),
         )
 
-    def test_no_dateutil_module(self):
-        with patch("alembic.script.base.tz", new=None):
+    def test_no_zoneinfo_module(self):
+        with patch("alembic.script.base.ZoneInfo", new=None):
             with expect_raises_message(
-                CommandError, "The library 'python-dateutil' is required"
+                CommandError,
+                "Python >= 3.9 is required for timezone support or"
+                "the 'backports.zoneinfo' package must be installed.",
             ):
                 self._test_tz(
                     "utc",
diff --git a/tox.ini b/tox.ini
index 24219bb4620dccde3ab8f92a6d28aa62d00c5056..ef6a8ddd8b7baafffd3f354a48658d4fcb1f583b 100644 (file)
--- a/tox.ini
+++ b/tox.ini
@@ -22,7 +22,7 @@ deps=pytest>4.6
      cov: pytest-cov
      sqlalchemy: sqlalchemy>=1.3.0
      mako
-     python-dateutil
+     backports.zoneinfo;python_version<"3.9"
      zimports
      black==23.3.0
      greenlet>=1
@@ -74,7 +74,6 @@ deps=
     sqlalchemy>=2
     mako
     types-pkg-resources
-    types-python-dateutil
     # is imported in alembic/testing and mypy complains if it's not installed.
     pytest
 commands = mypy ./alembic/ --exclude alembic/templates