Update tzlocal to 2.0.0

This commit is contained in:
JonnyWong16 2020-03-19 20:30:44 -07:00
parent b9a80d06e4
commit 2917b609c3
6 changed files with 166 additions and 174 deletions

View file

@ -1,6 +1,47 @@
Changes
=======
2.0.0 (2019-07-23)
------------------
- No differences since 2.0.0b3
Major differences since 1.5.1
.............................
- When no time zone configuration can be find, tzlocal now return UTC.
This is a major difference from 1.x, where an exception would be raised.
This change is because Docker images often have no configuration at all,
and the unix utilities will then default to UTC, so we follow that.
- If tzlocal on Unix finds a timezone name in a /etc config file, then
tzlocal now verifies that the timezone it fouds has the same offset as
the local computer is configured with. If it doesn't, something is
configured incorrectly. (Victor Torres, regebro)
- Get timezone via Termux `getprop` wrapper on Android. It's not officially
supported because we can't test it, but at least we make an effort.
(Jean Jordaan)
Minor differences and bug fixes
...............................
- Skip comment lines when parsing /etc/timezone. (Edward Betts)
- Don't load timezone from current directory. (Gabriel Corona)
- Now verifies that the config files actually contain something before
reading them. (Zackary Welch, regebro)
- Got rid of a BytesWarning (Mickaël Schoentgen)
- Now handles if config file paths exists, but are directories.
- Moved tests out from distributions
- Support wheels
1.5.1 (2017-12-01)
------------------
@ -38,9 +79,6 @@ Changes
------------------
- Ensure closing of subprocess on OS X (ayalash)
DOING: Implementing feedback on the unsubscribe button
DOING: Investigating remaining issues with DOCX export
BLOCKERS: None
- Removed unused imports (jwilk)

View file

@ -1,134 +0,0 @@
import mock
import os
import pytz
import sys
import tzlocal.unix
import unittest
from datetime import datetime
class TzLocalTests(unittest.TestCase):
def setUp(self):
if 'TZ' in os.environ:
del os.environ['TZ']
self.path = os.path.split(__file__)[0]
def test_env(self):
tz_harare = tzlocal.unix._tz_from_env(':Africa/Harare')
self.assertEqual(tz_harare.zone, 'Africa/Harare')
# Some Unices allow this as well, so we must allow it:
tz_harare = tzlocal.unix._tz_from_env('Africa/Harare')
self.assertEqual(tz_harare.zone, 'Africa/Harare')
tz_local = tzlocal.unix._tz_from_env(':' + os.path.join(self.path, 'test_data', 'Harare'))
self.assertEqual(tz_local.zone, 'local')
# Make sure the local timezone is the same as the Harare one above.
# We test this with a past date, so that we don't run into future changes
# of the Harare timezone.
dt = datetime(2012, 1, 1, 5)
self.assertEqual(tz_harare.localize(dt), tz_local.localize(dt))
# Non-zoneinfo timezones are not supported in the TZ environment.
self.assertRaises(pytz.UnknownTimeZoneError, tzlocal.unix._tz_from_env, 'GMT+03:00')
# Test the _try function
os.environ['TZ'] = 'Africa/Harare'
tz_harare = tzlocal.unix._try_tz_from_env()
self.assertEqual(tz_harare.zone, 'Africa/Harare')
# With a zone that doesn't exist
os.environ['TZ'] = 'Just Nonsense'
tz_harare = tzlocal.unix._try_tz_from_env()
self.assertIsNone(tz_harare)
def test_timezone(self):
# Most versions of Ubuntu
tz = tzlocal.unix._get_localzone(_root=os.path.join(self.path, 'test_data', 'timezone'))
self.assertEqual(tz.zone, 'Africa/Harare')
def test_zone_setting(self):
# A ZONE setting in /etc/sysconfig/clock, f ex CentOS
tz = tzlocal.unix._get_localzone(_root=os.path.join(self.path, 'test_data', 'zone_setting'))
self.assertEqual(tz.zone, 'Africa/Harare')
def test_timezone_setting(self):
# A ZONE setting in /etc/conf.d/clock, f ex Gentoo
tz = tzlocal.unix._get_localzone(_root=os.path.join(self.path, 'test_data', 'timezone_setting'))
self.assertEqual(tz.zone, 'Africa/Harare')
def test_symlink_localtime(self):
# A ZONE setting in the target path of a symbolic linked localtime, f ex systemd distributions
tz = tzlocal.unix._get_localzone(_root=os.path.join(self.path, 'test_data', 'symlink_localtime'))
self.assertEqual(tz.zone, 'Africa/Harare')
def test_vardbzoneinfo_setting(self):
# A ZONE setting in /etc/conf.d/clock, f ex Gentoo
tz = tzlocal.unix._get_localzone(_root=os.path.join(self.path, 'test_data', 'vardbzoneinfo'))
self.assertEqual(tz.zone, 'Africa/Harare')
def test_only_localtime(self):
tz = tzlocal.unix._get_localzone(_root=os.path.join(self.path, 'test_data', 'localtime'))
self.assertEqual(tz.zone, 'local')
dt = datetime(2012, 1, 1, 5)
self.assertEqual(pytz.timezone('Africa/Harare').localize(dt), tz.localize(dt))
def test_get_reload(self):
os.environ['TZ'] = 'Africa/Harare'
tz_harare = tzlocal.unix.get_localzone()
self.assertEqual(tz_harare.zone, 'Africa/Harare')
# Changing the TZ makes no difference, because it's cached
os.environ['TZ'] = 'Africa/Johannesburg'
tz_harare = tzlocal.unix.get_localzone()
self.assertEqual(tz_harare.zone, 'Africa/Harare')
# So we reload it
tz_harare = tzlocal.unix.reload_localzone()
self.assertEqual(tz_harare.zone, 'Africa/Johannesburg')
def test_fail(self):
with self.assertRaises(pytz.exceptions.UnknownTimeZoneError):
tz = tzlocal.unix._get_localzone(_root=os.path.join(self.path, 'test_data'))
if sys.platform == 'win32':
import tzlocal.win32
class TzWin32Tests(unittest.TestCase):
def test_win32(self):
tzlocal.win32.get_localzone()
else:
class TzWin32Tests(unittest.TestCase):
def test_win32_on_unix(self):
# Yes, winreg is all mocked out, but this test means we at least
# catch syntax errors, etc.
winreg = mock.MagicMock()
winreg.OpenKey = mock.MagicMock()
winreg.OpenKey.close = mock.MagicMock()
winreg.QueryInfoKey = mock.MagicMock(return_value=(1, 1))
winreg.EnumValue = mock.MagicMock(
return_value=('TimeZoneKeyName','Belarus Standard Time'))
winreg.EnumKey = mock.Mock(return_value='Bahia Standard Time')
sys.modules['winreg'] = winreg
import tzlocal.win32
tz = tzlocal.win32.get_localzone()
self.assertEqual(tz.zone, 'Europe/Minsk')
tzlocal.win32.valuestodict = mock.Mock(return_value={
'StandardName': 'Mocked Standard Time',
'Std': 'Mocked Standard Time',
})
tz = tzlocal.win32.reload_localzone()
self.assertEqual(tz.zone, 'America/Bahia')
if __name__ == '__main__':
unittest.main()

View file

@ -1,15 +1,19 @@
import os
import re
import pytz
import re
import warnings
from tzlocal import utils
_cache_tz = None
def _tz_from_env(tzenv):
if tzenv[0] == ':':
tzenv = tzenv[1:]
# TZ specifies a file
if os.path.exists(tzenv):
if os.path.isabs(tzenv) and os.path.exists(tzenv):
with open(tzenv, 'rb') as tzfile:
return pytz.tzfile.build_tzinfo('local', tzfile)
@ -48,40 +52,60 @@ def _get_localzone(_root='/'):
if tzenv:
return tzenv
# Are we under Termux on Android?
if os.path.exists('/system/bin/getprop'):
import subprocess
androidtz = subprocess.check_output(['getprop', 'persist.sys.timezone']).strip().decode()
return pytz.timezone(androidtz)
# Now look for distribution specific configuration files
# that contain the timezone name.
for configfile in ('etc/timezone', 'var/db/zoneinfo'):
tzpath = os.path.join(_root, configfile)
if os.path.exists(tzpath):
try:
with open(tzpath, 'rb') as tzfile:
data = tzfile.read()
# Issue #3 was that /etc/timezone was a zoneinfo file.
# That's a misconfiguration, but we need to handle it gracefully:
if data[:5] == 'TZif2':
if data[:5] == b'TZif2':
continue
etctz = data.strip().decode()
if not etctz:
# Empty file, skip
continue
for etctz in data.decode().splitlines():
# Get rid of host definitions and comments:
if ' ' in etctz:
etctz, dummy = etctz.split(' ', 1)
if '#' in etctz:
etctz, dummy = etctz.split('#', 1)
return pytz.timezone(etctz.replace(' ', '_'))
if not etctz:
continue
tz = pytz.timezone(etctz.replace(' ', '_'))
if _root == '/':
# We are using a file in etc to name the timezone.
# Verify that the timezone specified there is actually used:
utils.assert_tz_offset(tz)
return tz
except IOError:
# File doesn't exist or is a directory
continue
# 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('\s*ZONE\s*=\s*\"')
timezone_re = re.compile('\s*TIMEZONE\s*=\s*\"')
zone_re = re.compile(r'\s*ZONE\s*=\s*\"')
timezone_re = re.compile(r'\s*TIMEZONE\s*=\s*\"')
end_re = re.compile('\"')
for filename in ('etc/sysconfig/clock', 'etc/conf.d/clock'):
tzpath = os.path.join(_root, filename)
if not os.path.exists(tzpath):
continue
try:
with open(tzpath, 'rt') as tzfile:
data = tzfile.readlines()
@ -97,7 +121,16 @@ def _get_localzone(_root='/'):
etctz = line[:end_re.search(line).start()]
# We found a timezone
return pytz.timezone(etctz.replace(' ', '_'))
tz = pytz.timezone(etctz.replace(' ', '_'))
if _root == '/':
# We are using a file in etc to name the timezone.
# Verify that the timezone specified there is actually used:
utils.assert_tz_offset(tz)
return tz
except IOError:
# File doesn't exist or is a directory
continue
# systemd distributions use symlinks that include the zone name,
# see manpage of localtime(5) and timedatectl(1)
@ -122,15 +155,18 @@ def _get_localzone(_root='/'):
with open(tzpath, 'rb') as tzfile:
return pytz.tzfile.build_tzinfo('local', tzfile)
raise pytz.UnknownTimeZoneError('Can not find any timezone configuration')
warnings.warn('Can not find any timezone configuration, defaulting to UTC.')
return pytz.utc
def get_localzone():
"""Get the computers configured local timezone, if any."""
global _cache_tz
if _cache_tz is None:
_cache_tz = _get_localzone()
return _cache_tz
def reload_localzone():
"""Reload the cached localzone. You need to call this if the timezone has changed."""
global _cache_tz

38
lib/tzlocal/utils.py Normal file
View file

@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
import datetime
def get_system_offset():
"""Get system's timezone offset using built-in library time.
For the Timezone constants (altzone, daylight, timezone, and tzname), the
value is determined by the timezone rules in effect at module load time or
the last time tzset() is called and may be incorrect for times in the past.
To keep compatibility with Windows, we're always importing time module here.
"""
import time
if time.daylight and time.localtime().tm_isdst > 0:
return -time.altzone
else:
return -time.timezone
def get_tz_offset(tz):
"""Get timezone's offset using built-in function datetime.utcoffset()."""
return int(datetime.datetime.now(tz).utcoffset().total_seconds())
def assert_tz_offset(tz):
"""Assert that system's timezone offset equals to the timezone offset found.
If they don't match, we probably have a misconfiguration, for example, an
incorrect timezone set in /etc/timezone file in systemd distributions."""
tz_offset = get_tz_offset(tz)
system_offset = get_system_offset()
if tz_offset != system_offset:
msg = ('Timezone offset does not match system offset: {0} != {1}. '
'Please, check your config files.').format(
tz_offset, system_offset
)
raise ValueError(msg)

View file

@ -3,11 +3,14 @@ try:
except ImportError:
import winreg
from tzlocal.windows_tz import win_tz
import pytz
from tzlocal.windows_tz import win_tz
from tzlocal import utils
_cache_tz = None
def valuestodict(key):
"""Convert a registry key's values to a dictionary."""
dict = {}
@ -17,6 +20,7 @@ def valuestodict(key):
dict[data[0]] = data[1]
return dict
def get_localzone_name():
# Windows is special. It has unique time zone names (in several
# meanings of the word) available, but unfortunately, they can be
@ -81,15 +85,20 @@ def get_localzone_name():
return timezone
def get_localzone():
"""Returns the zoneinfo-based tzinfo object that matches the Windows-configured timezone."""
global _cache_tz
if _cache_tz is None:
_cache_tz = pytz.timezone(get_localzone_name())
utils.assert_tz_offset(_cache_tz)
return _cache_tz
def reload_localzone():
"""Reload the cached localzone. You need to call this if the timezone has changed."""
global _cache_tz
_cache_tz = pytz.timezone(get_localzone_name())
utils.assert_tz_offset(_cache_tz)
return _cache_tz

View file

@ -99,10 +99,12 @@ win_tz = {'AUS Central Standard Time': 'Australia/Darwin',
'Saint Pierre Standard Time': 'America/Miquelon',
'Sakhalin Standard Time': 'Asia/Sakhalin',
'Samoa Standard Time': 'Pacific/Apia',
'Sao Tome Standard Time': 'Africa/Sao_Tome',
'Saratov Standard Time': 'Europe/Saratov',
'Singapore Standard Time': 'Asia/Singapore',
'South Africa Standard Time': 'Africa/Johannesburg',
'Sri Lanka Standard Time': 'Asia/Colombo',
'Sudan Standard Time': 'Africa/Khartoum',
'Syria Standard Time': 'Asia/Damascus',
'Taipei Standard Time': 'Asia/Taipei',
'Tasmania Standard Time': 'Australia/Hobart',
@ -164,7 +166,7 @@ tz_win = {'Africa/Abidjan': 'Greenwich Standard Time',
'Africa/Johannesburg': 'South Africa Standard Time',
'Africa/Juba': 'E. Africa Standard Time',
'Africa/Kampala': 'E. Africa Standard Time',
'Africa/Khartoum': 'E. Africa Standard Time',
'Africa/Khartoum': 'Sudan Standard Time',
'Africa/Kigali': 'South Africa Standard Time',
'Africa/Kinshasa': 'W. Central Africa Standard Time',
'Africa/Lagos': 'W. Central Africa Standard Time',
@ -185,7 +187,7 @@ tz_win = {'Africa/Abidjan': 'Greenwich Standard Time',
'Africa/Nouakchott': 'Greenwich Standard Time',
'Africa/Ouagadougou': 'Greenwich Standard Time',
'Africa/Porto-Novo': 'W. Central Africa Standard Time',
'Africa/Sao_Tome': 'Greenwich Standard Time',
'Africa/Sao_Tome': 'Sao Tome Standard Time',
'Africa/Timbuktu': 'Greenwich Standard Time',
'Africa/Tripoli': 'Libya Standard Time',
'Africa/Tunis': 'W. Central Africa Standard Time',
@ -285,7 +287,7 @@ tz_win = {'Africa/Abidjan': 'Greenwich Standard Time',
'America/Mendoza': 'Argentina Standard Time',
'America/Menominee': 'Central Standard Time',
'America/Merida': 'Central Standard Time (Mexico)',
'America/Metlakatla': 'Alaskan Standard Time',
'America/Metlakatla': 'Pacific Standard Time',
'America/Mexico_City': 'Central Standard Time (Mexico)',
'America/Miquelon': 'Saint Pierre Standard Time',
'America/Moncton': 'Atlantic Standard Time',
@ -345,7 +347,7 @@ tz_win = {'Africa/Abidjan': 'Greenwich Standard Time',
'America/Winnipeg': 'Central Standard Time',
'America/Yakutat': 'Alaskan Standard Time',
'America/Yellowknife': 'Mountain Standard Time',
'Antarctica/Casey': 'Central Pacific Standard Time',
'Antarctica/Casey': 'W. Australia Standard Time',
'Antarctica/Davis': 'SE Asia Standard Time',
'Antarctica/DumontDUrville': 'West Pacific Standard Time',
'Antarctica/Macquarie': 'Central Pacific Standard Time',
@ -386,7 +388,7 @@ tz_win = {'Africa/Abidjan': 'Greenwich Standard Time',
'Asia/Dili': 'Tokyo Standard Time',
'Asia/Dubai': 'Arabian Standard Time',
'Asia/Dushanbe': 'West Asia Standard Time',
'Asia/Famagusta': 'Turkey Standard Time',
'Asia/Famagusta': 'GTB Standard Time',
'Asia/Gaza': 'West Bank Standard Time',
'Asia/Harbin': 'China Standard Time',
'Asia/Hebron': 'West Bank Standard Time',
@ -421,7 +423,8 @@ tz_win = {'Africa/Abidjan': 'Greenwich Standard Time',
'Asia/Pontianak': 'SE Asia Standard Time',
'Asia/Pyongyang': 'North Korea Standard Time',
'Asia/Qatar': 'Arab Standard Time',
'Asia/Qyzylorda': 'Central Asia Standard Time',
'Asia/Qostanay': 'Central Asia Standard Time',
'Asia/Qyzylorda': 'West Asia Standard Time',
'Asia/Rangoon': 'Myanmar Standard Time',
'Asia/Riyadh': 'Arab Standard Time',
'Asia/Saigon': 'SE Asia Standard Time',
@ -530,6 +533,7 @@ tz_win = {'Africa/Abidjan': 'Greenwich Standard Time',
'Etc/GMT-7': 'SE Asia Standard Time',
'Etc/GMT-8': 'Singapore Standard Time',
'Etc/GMT-9': 'Tokyo Standard Time',
'Etc/UCT': 'UTC',
'Etc/UTC': 'UTC',
'Europe/Amsterdam': 'W. Europe Standard Time',
'Europe/Andorra': 'W. Europe Standard Time',
@ -673,6 +677,7 @@ tz_win = {'Africa/Abidjan': 'Greenwich Standard Time',
'ROK': 'Korea Standard Time',
'Singapore': 'Singapore Standard Time',
'Turkey': 'Turkey Standard Time',
'UCT': 'UTC',
'US/Alaska': 'Alaskan Standard Time',
'US/Aleutian': 'Aleutian Standard Time',
'US/Arizona': 'US Mountain Standard Time',