Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 16 additions & 54 deletions src/pendulum/tz/local_timezone.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import contextlib
import os
import re
import sys
import warnings

Expand Down Expand Up @@ -158,63 +157,26 @@ def _get_darwin_timezone() -> Timezone:


def _get_unix_timezone(_root: str = "/") -> Timezone:
tzenv = os.environ.get("TZ")
if tzenv:
if tzenv := os.environ.get("TZ"):
with contextlib.suppress(ValueError):
return _tz_from_env(tzenv)

# Now look for distribution specific configuration files
# that contain the timezone name.
tzpath = Path(_root) / "etc" / "timezone"
if tzpath.is_file():
tzfile_data = tzpath.read_bytes()
# Issue #3 was that /etc/timezone was a zoneinfo file.
# That's a misconfiguration, but we need to handle it gracefully:
if not tzfile_data.startswith(b"TZif2"):
etctz = tzfile_data.strip().decode()
# Get rid of host definitions and comments:
etctz, _, _ = etctz.partition(" ")
etctz, _, _ = etctz.partition("#")
return Timezone(etctz.replace(" ", "_"))

# CentOS has a ZONE setting in /etc/sysconfig/clock,
# OpenSUSE has a TIMEZONE setting in /etc/sysconfig/clock and
# Gentoo has a TIMEZONE setting in /etc/conf.d/clock
# We look through these files for a timezone:
zone_re = re.compile(r'\s*(TIME)?ZONE\s*=\s*"([^"]+)?"')

for filename in ("etc/sysconfig/clock", "etc/conf.d/clock"):
tzpath = Path(_root) / filename
if tzpath.is_file():
data = tzpath.read_text().splitlines()
for line in data:
# Look for the ZONE= or TIMEZONE= setting.
match = zone_re.match(line)
if match:
etctz = match.group(2)
parts = list(reversed(etctz.replace(" ", "_").split(os.path.sep)))
tzpath_parts: list[str] = []
while parts:
tzpath_parts.insert(0, parts.pop(0))
with contextlib.suppress(InvalidTimezone):
return Timezone(os.path.sep.join(tzpath_parts))

# systemd distributions use symlinks that include the zone name,
# see manpage of localtime(5) and timedatectl(1)
tzpath = Path(_root) / "etc" / "localtime"
if tzpath.is_file() and tzpath.is_symlink():
parts = [p.replace(" ", "_") for p in reversed(tzpath.resolve().parts)]
tzpath_parts: list[str] = [] # type: ignore[no-redef]
while parts:
tzpath_parts.insert(0, parts.pop(0))
with contextlib.suppress(InvalidTimezone):
return Timezone(os.path.sep.join(tzpath_parts))
localtime = Path(_root) / "etc" / "localtime"

# No explicit setting existed. Use localtime
for filename in ("etc/localtime", "usr/local/etc/localtime"):
tzpath = Path(_root) / filename
if tzpath.is_file():
with tzpath.open("rb") as f:
# Symlink → IANA name
if localtime.is_symlink() and localtime.is_file():
parts = [p.replace(" ", "_") for p in localtime.resolve().parts]
for i in range(1, len(parts) + 1):
with contextlib.suppress(InvalidTimezone):
return Timezone(os.path.sep.join(parts[-i:]))

# Fallback: read tzfile data
for tzfile in (
localtime,
Path(_root) / "usr/local/etc/localtime",
):
if tzfile.is_file():
with tzfile.open("rb") as f:
return Timezone.from_file(f)

warnings.warn(
Expand Down
12 changes: 0 additions & 12 deletions tests/tz/test_local_timezone.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,6 @@ def test_unix_symlink():
assert tz.name == "Europe/Paris"


@pytest.mark.skipif(
sys.platform == "win32", reason="Test only available for UNIX systems"
)
def test_unix_clock():
# A ZONE setting in the target path of a symbolic linked localtime,
# f ex systemd distributions
local_path = os.path.join(os.path.split(__file__)[0], "..")
tz = _get_unix_timezone(_root=os.path.join(local_path, "fixtures", "tz", "clock"))

assert tz.name == "Europe/Zurich"


@pytest.mark.skipif(sys.platform != "win32", reason="Test only available for Windows")
def test_windows_timezone():
timezone = _get_windows_timezone()
Expand Down