From 9c1af0c674a8dc9a12e3fdc6e29f26b9fd24c197 Mon Sep 17 00:00:00 2001 From: Michael Klatt Date: Thu, 1 Jun 2023 11:10:27 -0500 Subject: [PATCH] Fixed localtime._unix._get_localzone() (#990) In certain OS installations, the target of /etc/localtime contains double slashes. This is a valid path, but not a valid zoneinfo key. This fix replaces `os.readlink` with `os.path.realpath`, which is guaranteed to return a normalized path. Fixes #990 --- babel/localtime/_unix.py | 5 ++++- setup.py | 1 + tests/test_localtime.py | 19 +++++++++++++------ 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/babel/localtime/_unix.py b/babel/localtime/_unix.py index eb81beb61..ba13555c0 100644 --- a/babel/localtime/_unix.py +++ b/babel/localtime/_unix.py @@ -39,7 +39,10 @@ def _get_localzone(_root: str = '/') -> datetime.tzinfo: # zone on operating systems like OS X. On OS X especially this is the # only one that actually works. try: - link_dst = os.readlink('/etc/localtime') + # Make sure the path is normalized so that it will be parsed correctly + # below, e.g. 'Etc//UTC' is a valid path but not a valid zoneinfo key. + # + link_dst = os.path.realpath('/etc/localtime') except OSError: pass else: diff --git a/setup.py b/setup.py index e168f0937..e9cd132fc 100755 --- a/setup.py +++ b/setup.py @@ -71,6 +71,7 @@ def run(self): 'dev': [ 'pytest>=6.0', 'pytest-cov', + 'pyfakefs~=5.0', 'freezegun~=1.0', ], }, diff --git a/tests/test_localtime.py b/tests/test_localtime.py index 3f87bf1ad..8b49545b7 100644 --- a/tests/test_localtime.py +++ b/tests/test_localtime.py @@ -5,13 +5,13 @@ """ import datetime -import pytest -import sys import os.path +import sys from pathlib import Path -from babel.localtime import get_localzone, LOCALTZ +import pytest +from babel.localtime import LOCALTZ, get_localzone _timezones = { "Etc/UTC": "UTC", @@ -60,9 +60,16 @@ def timezone(self, zoneinfo, request) -> str: # standard-ish Unix implementation where `/etc/localtime` can be used # to set the time zone. # + # + # Note the double slash in the symlink source. Because os.symlink() + # does not normalize paths, this will appear on the file system as + # e.g. `zoneinfo//Etc/UTC`. Although irregular, this is a valid link + # and has been observed in certain Ubuntu installations, so make sure + # 'localtime' can handle them. + # . key, name = request.param os.remove("/etc/localtime") - os.symlink(f"{zoneinfo}/{key}", "/etc/localtime") + os.symlink(f"{zoneinfo}//{key}", "/etc/localtime") # double slash OK return name def test_get_localzone(self, zoneinfo, timezone): @@ -76,7 +83,7 @@ def test_localtz(self, zoneinfo): """ Test the LOCALTZ module attribute. """ - assert LOCALTZ == get_localzone() + assert get_localzone() == LOCALTZ @pytest.mark.skipif(sys.platform != "win32", reason="Win32 tests") @@ -90,4 +97,4 @@ def test_localtz(self): """ Test the LOCALTZ module attribute. """ - assert LOCALTZ == get_localzone() + assert get_localzone() == LOCALTZ