Skip to content

Commit

Permalink
Use zoneinfo instead of dateutil.
Browse files Browse the repository at this point in the history
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
  • Loading branch information
CaselIT authored and zzzeek committed Nov 18, 2023
1 parent 885f29a commit 70e1fba
Show file tree
Hide file tree
Showing 10 changed files with 76 additions and 46 deletions.
35 changes: 22 additions & 13 deletions alembic/script/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from . import write_hooks
from .. import util
from ..runtime import migration
from ..util import compat
from ..util import not_none

if TYPE_CHECKING:
Expand All @@ -35,9 +36,14 @@
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)$")
Expand Down Expand Up @@ -604,23 +610,26 @@ def _ensure_directory(self, path: str) -> None:

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:
Expand Down
6 changes: 3 additions & 3 deletions alembic/templates/async/alembic.ini.mako
Original file line number Diff line number Diff line change
Expand Up @@ -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 =

Expand Down
6 changes: 3 additions & 3 deletions alembic/templates/generic/alembic.ini.mako
Original file line number Diff line number Diff line change
Expand Up @@ -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 =

Expand Down
6 changes: 3 additions & 3 deletions alembic/templates/multidb/alembic.ini.mako
Original file line number Diff line number Diff line change
Expand Up @@ -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 =

Expand Down
2 changes: 1 addition & 1 deletion docs/build/requirements.txt
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
20 changes: 11 additions & 9 deletions docs/build/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 =

Expand Down Expand Up @@ -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.
Expand Down
9 changes: 9 additions & 0 deletions docs/build/unreleased/1339.rst
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 22 additions & 11 deletions tests/test_script_production.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import re
from unittest.mock import patch

from dateutil import tz
import sqlalchemy as sa
from sqlalchemy import Column
from sqlalchemy import inspect
Expand Down Expand Up @@ -41,6 +40,11 @@
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


Expand Down Expand Up @@ -189,12 +193,16 @@ def test_args(self):

@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",
),
Expand Down Expand Up @@ -227,7 +235,8 @@ def _test_tz(self, timezone_arg, given, expected):
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()
Expand All @@ -238,7 +247,7 @@ def test_custom_tz(self):
"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")
),
)

Expand All @@ -247,23 +256,23 @@ def test_custom_tz_lowercase(self):
"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")
),
)

def test_custom_tz_utc(self):
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):
self._test_tz(
"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")
),
)

Expand All @@ -284,10 +293,12 @@ def test_tz_cant_locate(self):
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",
Expand Down
3 changes: 1 addition & 2 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 70e1fba

Please sign in to comment.