From dbf43652600e79cde2b3e943c2344953bb50c4a9 Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Tue, 11 Jun 2019 15:04:41 -0400 Subject: [PATCH 01/20] Fix tzabbr support for recent versions of dateutil dateutil expects tzinfos to return an actual tzinfo object, which is causing failures with recent versions. This switches tzabbr over to acting as a proxy for its associated pytz zone rather than a passive label. --- datetime_tz/__init__.py | 2 +- datetime_tz/pytz_abbr.py | 53 +++++++++++++++++++++++++++++++--------- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/datetime_tz/__init__.py b/datetime_tz/__init__.py index a4387ce..8e61295 100644 --- a/datetime_tz/__init__.py +++ b/datetime_tz/__init__.py @@ -691,7 +691,7 @@ def smartparse(cls, toparse, tzinfo=None): if isinstance(dt.tzinfo, pytz_abbr.tzabbr): abbr = dt.tzinfo dt = dt.replace(tzinfo=None) - dt = cls(dt, abbr.zone, is_dst=abbr.dst) + dt = cls(dt, abbr.zone, is_dst=abbr.is_dst) dt = cls(dt) diff --git a/datetime_tz/pytz_abbr.py b/datetime_tz/pytz_abbr.py index c97e460..fb63b39 100644 --- a/datetime_tz/pytz_abbr.py +++ b/datetime_tz/pytz_abbr.py @@ -51,15 +51,51 @@ class tzabbr(datetime.tzinfo): """A timezone abbreviation. - *WARNING*: This is not a tzinfo implementation! Trying to use this as tzinfo - object will result in failure. We inherit from datetime.tzinfo so we can get - through the dateutil checks. + *WARNING*: This is a proxy object for an underlying `pytz` object only + intended to allow dateutil to probe the localized datetime for information. + datetime_tz's parsing logic will replace it with the relevant pytz zone + before returning to the end user. """ - pass + + def __init__(self, abbr, name, region, zone, dst): + self.abbr = abbr + self.name = name + self.region = region + + if not isinstance(zone, pytz.tzinfo.BaseTzInfo): + zone = pytz.timezone(zone) + + self.zone = zone + self.is_dst = dst + + def _get_localized(self, dt): + # To make this a fully-functioning pass-through to the underlying pytz + # zone, we would want to use `fold` to set `is_dst`, but since this is + # only a temporary proxy for the zone, we will fix the DST status + return self.zone.localize(dt.replace(tzinfo=None), is_dst=self.is_dst) + + def tzname(self, dt): + return self._get_localized(dt).tzname() + + def utcoffset(self, dt): + return self._get_localized(dt).utcoffset() # pragma: no cover + + def dst(self, dt): + return self._get_localized(dt).dst() # pragma: no cover # A "marker" tzinfo object which is used to signify an unknown timezone. -unknown = datetime.tzinfo() +class _UnknownZone(datetime.tzinfo): + def tzname(self, dt): + return "UNK" + + def dst(self, dt): + return None + + def utcoffset(self, dt): + return datetime.timedelta(0) # pragma: no cover + +unknown = _UnknownZone() regions = {"all": {}, "military": {}} @@ -74,12 +110,7 @@ def tzabbr_register(abbr, name, region, zone, dst): If another abbreviation with the same name has already been registered it new abbreviation will only be registered in region specific dictionary. """ - newabbr = tzabbr() - newabbr.abbr = abbr - newabbr.name = name - newabbr.region = region - newabbr.zone = zone - newabbr.dst = dst + newabbr = tzabbr(abbr, name, region, zone, dst) if abbr not in all: all[abbr] = newabbr From 5f35f9a2e716f698580d3236fab91adae313d079 Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Tue, 11 Jun 2019 15:06:37 -0400 Subject: [PATCH 02/20] Add pyproject.toml for PEP 517 support --- pyproject.toml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..dff4a2b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,9 @@ +[build-system] +requires = [ + "setuptools>=41.0", + "wheel", + "Genshi", + "python-dateutil>=2.0", + "pytz >= 2011g", +] +build-backend = "setuptools.build_meta" From d0731a9ba8ccfd8119320f998730b0b62364dde7 Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Tue, 11 Jun 2019 15:57:03 -0400 Subject: [PATCH 03/20] Update CLDR URL --- datetime_tz/update_win32tz_map.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datetime_tz/update_win32tz_map.py b/datetime_tz/update_win32tz_map.py index ad479b5..1111d78 100644 --- a/datetime_tz/update_win32tz_map.py +++ b/datetime_tz/update_win32tz_map.py @@ -46,7 +46,7 @@ win32tz_map = None -_CLDR_WINZONES_URL = "http://www.unicode.org/repos/cldr/trunk/common/supplemental/windowsZones.xml" # pylint: disable=line-too-long +_CLDR_WINZONES_URL = "https://github.com/unicode-org/cldr/raw/master/common/supplemental/windowsZones.xml" # pylint: disable=line-too-long def download_cldr_win32tz_map_xml(): From 135dab6ca1e63f4d2fd4ccd2acf656b809529e93 Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Tue, 11 Jun 2019 15:59:45 -0400 Subject: [PATCH 04/20] Convert setup.py to use PEP 517 --- setup.py | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/setup.py b/setup.py index 44f572d..ef23089 100644 --- a/setup.py +++ b/setup.py @@ -18,13 +18,13 @@ # import os +import sys + +from setuptools import setup +from setuptools.command import sdist, install -try: - from setuptools import setup - from setuptools.command import sdist, install -except ImportError: - from distutils.core import setup - from distutils.command import sdist, install +# Required in order to import datetime_tz in PEP 517 builds +sys.path.append(os.path.dirname(__file__)) class update_sdist(sdist.sdist): def run(self): @@ -40,7 +40,6 @@ def run(self): update_win32tz_map.update_stored_win32tz_map() install.install.run(self) -import sys data = dict( name='python-datetime-tz', @@ -60,22 +59,11 @@ def run(self): "Topic :: Software Development :: Internationalization", ], packages=['datetime_tz'], - install_requires=[], - setup_requires=['Genshi'], + install_requires=["pytz >= 2011g", "python-dateutil >= 2.0"], py_modules=['datetime_tz','datetime_tz.pytz_abbr'], test_suite='tests', cmdclass={'sdist': update_sdist, "install": update_install}, ) -deps = [] -if sys.version[:3] < '3.0': - deps += ['pytz >= 2007g'] - deps += ['python-dateutil >= 1.4'] -else: - deps += ['pytz >= 2011g'] - deps += ['python-dateutil >= 2.0'] - -data['install_requires'] += deps -data['setup_requires'] += deps setup(**data) From 5b3612bbd8cc8ac06997b3b52cf30a3110848ddc Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Tue, 11 Jun 2019 15:59:58 -0400 Subject: [PATCH 05/20] Use tox to run tests --- .gitignore | 1 + datetime_tz/pytz_abbr.py | 10 +++++++++- tox.ini | 15 +++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index 0dccebb..ef80c71 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ build dist *.egg* *.py[co] +.tox docs/_* datetime_tz/win32tz_map.py py2 diff --git a/datetime_tz/pytz_abbr.py b/datetime_tz/pytz_abbr.py index fb63b39..3470324 100644 --- a/datetime_tz/pytz_abbr.py +++ b/datetime_tz/pytz_abbr.py @@ -47,6 +47,12 @@ import pytz import pytz.tzfile +try: + basestring +except NameError: + # pylint: disable=redefined-builtin + basestring = str + class tzabbr(datetime.tzinfo): """A timezone abbreviation. @@ -58,11 +64,13 @@ class tzabbr(datetime.tzinfo): """ def __init__(self, abbr, name, region, zone, dst): + super(tzabbr, self).__init__() + self.abbr = abbr self.name = name self.region = region - if not isinstance(zone, pytz.tzinfo.BaseTzInfo): + if isinstance(zone, basestring): zone = pytz.timezone(zone) self.zone = zone diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..8c66fde --- /dev/null +++ b/tox.ini @@ -0,0 +1,15 @@ +[tox] +envlist = py27, + py36, + py37 +minversion = 3.3.0 +skip_missing_interpreters = true +isolated_build = true + +[testenv] +description = run the unit tests with pytest under {basepython} +commands = pytest {posargs} tests.py +deps = + -rrequirements.txt + pytest + From 1d7c64742d8b7d402fb9c1752a0a9d3a2bf662ae Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Tue, 11 Jun 2019 16:01:31 -0400 Subject: [PATCH 06/20] Update release script This should be able to build a single universal wheel, so there is no need for separate Python 2 and Python 3 releases. This also uses PEP 517 for the builds and drops invocations of setup.py --- .gitignore | 3 +-- release.sh | 42 ++++++++++++------------------------------ 2 files changed, 13 insertions(+), 32 deletions(-) diff --git a/.gitignore b/.gitignore index ef80c71..9a2ab92 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ dist .tox docs/_* datetime_tz/win32tz_map.py -py2 -py3 +release_venv venv venv3 diff --git a/release.sh b/release.sh index 29c71b7..ff0c182 100755 --- a/release.sh +++ b/release.sh @@ -1,33 +1,15 @@ -#!/bin/bash - -rm -rf py2 -virtualenv-2.7 py2 +#!/bin/bash -e +rm -rf release_env +rm -rf dist/ +python3 -m virtualenv release_env +source release_env/bin/activate ( - . py2/bin/activate - pip install --upgrade pip - pip install --upgrade setuptools - pip install -r requirements.txt - pip install wheel - python setup.py clean - python setup.py sdist - python setup.py bdist_wheel - python setup.py sdist upload - python setup.py bdist_wheel upload - #PS1="py2setup # " bash --norc -) +pip install -U pip +pip install -U twine +pip install -U pep517 +# Run the build +python -m pep517.build . -b -s -rm -rf py3 -virtualenv-3.4 py3 -( - . py3/bin/activate - pip install --upgrade pip - pip install --upgrade setuptools - pip install wheel - pip install -r requirements.txt - python setup.py clean - python setup.py sdist - python setup.py bdist_wheel - python setup.py sdist upload - python setup.py bdist_wheel upload - #PS1="py3setup # " bash --norc +# Upload the files with twine +twine upload dist/* ) From 43cfdf8de43903838beade8e76c2970a166ffa58 Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Tue, 11 Jun 2019 16:04:12 -0400 Subject: [PATCH 07/20] Remove "run_tests_all_pytz" If this is necessary, it should probably be done in a CI stage, or with parametrized tox environments. --- run_tests_all_pytz.py | 148 ------------------------------------------ 1 file changed, 148 deletions(-) delete mode 100755 run_tests_all_pytz.py diff --git a/run_tests_all_pytz.py b/run_tests_all_pytz.py deleted file mode 100755 index 6b26ac3..0000000 --- a/run_tests_all_pytz.py +++ /dev/null @@ -1,148 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# vim: set ts=2 sw=2 et sts=2 ai: -# -# Copyright 2015 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -# pylint doesn't understand the from __future__ import which must be the first -# line of the file. -# pylint: disable=missing-docstring,pointless-string-statement -# pylint: disable=g-statement-before-imports - -from __future__ import print_function - -"""Run the tests against every pytz version available.""" - -__author__ = "tansell@google.com (Tim Ansell)" - -import glob -import os -import os.path -import subprocess -import sys - -try: - # pylint: disable=g-import-not-at-top - from urllib.request import urlopen -except ImportError: - # pylint: disable=g-import-not-at-top - from urllib import urlopen - -try: - # pylint: disable=g-import-not-at-top - import simplejson -except ImportError: - # pylint: disable=g-import-not-at-top - import json as simplejson - - -if not hasattr(sys, "real_prefix"): - print("""\ -This script should only be run inside a virtualenv because it is going to -modify the install version of things. -""") - sys.exit(1) - -CACHE_DIR = os.path.expanduser(os.path.join("~", ".cache", "pypi")) -if not os.path.exists(CACHE_DIR): - os.makedirs(CACHE_DIR) -print("Using a download cache directory of", repr(CACHE_DIR)) - -# Get the pytz versions from pypi -pypi_data_raw = urlopen( - "https://pypi.python.org/pypi/pytz/json").read().decode("utf-8") -pypi_data = simplejson.loads(pypi_data_raw) - - -# Hack to work around https://github.com/pypa/pip/issues/2902 -def mangle_release(rel): - if rel < "2007g": - return None - - # pytz is only supported on Python 3 from version 2011b. - if sys.version[:3] >= "3.0" and rel < "2011g": - return None - - if rel.endswith("r"): - return rel[:-1]+".post0" - return rel - -releases = pypi_data["releases"] -# Download the pytz versions into the cache. -for release in sorted(releases): - # These lines shouldn't be needed but pypi always runs setup.py even when - # downloading. - filename = "*pytz-"+release+"*" - if glob.glob(os.path.join(CACHE_DIR, filename)): - print("Not downloading release", release, "(already downloaded).") - continue - - mangled = mangle_release(release) - if not mangled: - continue - - print("Downloading pytz release", release) - print("="*75) - subprocess.check_call("""\ -pip install \ - --pre \ - --no-binary all \ - --download %s \ - pytz==%s -""" % (CACHE_DIR, mangled), shell=True) - print("-"*75) - -if "--download-only" in sys.argv: - sys.exit(0) - -success = [] -failures = [] -for release in sorted(releases): - mangled = mangle_release(release) - if not mangled: - print("Skipping release", release, "as it is isn't supported") - continue - - print() - print("Running tests with pytz release", release) - print("="*75) - print("Installing...") - subprocess.check_call("""\ -pip install \ - --pre \ - --no-binary all \ - --no-index \ - --find-links=file://%s \ - pytz==%s -""" % (CACHE_DIR, mangled), shell=True) - print("-"*75) - print("Running tests...") - t = subprocess.Popen("python setup.py test", shell=True) - if t.wait() != 0: - failures.append(release) - else: - success.append(release) - print("="*75) - -print("Tests passed on pytz versions:") -print(success) -print() -print("Tests failed on pytz versions:") -print(failures) -print("="*75) - -if failures: - sys.exit(1) From 09810814aa2270a1f5a5bbae3bf9c895f308971f Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Tue, 11 Jun 2019 16:20:11 -0400 Subject: [PATCH 08/20] Add coverage metrics to tox job --- .gitignore | 1 + tox.ini | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9a2ab92..e456c8f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ dist *.egg* *.py[co] .tox +.coverage docs/_* datetime_tz/win32tz_map.py release_venv diff --git a/tox.ini b/tox.ini index 8c66fde..11acaa4 100644 --- a/tox.ini +++ b/tox.ini @@ -8,8 +8,9 @@ isolated_build = true [testenv] description = run the unit tests with pytest under {basepython} -commands = pytest {posargs} tests.py +commands = pytest {posargs: --cov=datetime_tz} tests.py deps = -rrequirements.txt pytest + pytest-cov From 3fe200eddc4f813f30176b0c5bfce77ed307ee1b Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Tue, 11 Jun 2019 16:20:36 -0400 Subject: [PATCH 09/20] Update Travis CI --- .travis.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index b1c557a..019917b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,19 @@ language: python python: - - 2.6 - 2.7 - - 3.3 - - 3.4 + - 3.6 - nightly matrix: + include: + # Required to run Python 3.7 + - python: 3.7 + dist: xenial + sudo: required allow_failures: - python: nightly + sudo: false cache: @@ -19,15 +23,11 @@ cache: install: - pip install --upgrade pip - - pip install pyopenssl ndg-httpsclient pyasn1 - - pip install -r requirements.txt - - python setup.py install - - python run_tests_all_pytz.py --download-only + - pip install --upgrade tox script: - - python run_tests_all_pytz.py + - tox -e py after_success: - pip install coveralls - - coverage run --source=datetime_tz setup.py test - - coveralls + - coveralls # Coverage generated by tox run From f3cabe12185f9520b33443b381511b5f84a8aba4 Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Tue, 11 Jun 2019 20:41:54 -0400 Subject: [PATCH 10/20] Add tests for multiple pytz versions --- .travis.yml | 1 + test_multiple_pytz_versions.sh | 9 +++++++++ tox.ini | 5 +++-- 3 files changed, 13 insertions(+), 2 deletions(-) create mode 100755 test_multiple_pytz_versions.sh diff --git a/.travis.yml b/.travis.yml index 019917b..9bbbb95 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,6 +26,7 @@ install: - pip install --upgrade tox script: + - ./test_multiple_pytz_versions.sh - tox -e py after_success: diff --git a/test_multiple_pytz_versions.sh b/test_multiple_pytz_versions.sh new file mode 100755 index 0000000..80e9c91 --- /dev/null +++ b/test_multiple_pytz_versions.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -e + +versions=( 2011g 2014.1 2016.1 2016.6.1 2017.2 2018.3 2018.9 ) +for version in ${versions[@]}; do + PYTZ_VERSION="==$version" tox -e py +done + + diff --git a/tox.ini b/tox.ini index 11acaa4..54dec6c 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,8 @@ isolated_build = true description = run the unit tests with pytest under {basepython} commands = pytest {posargs: --cov=datetime_tz} tests.py deps = - -rrequirements.txt + python-dateutil + pytz{env:PYTZ_VERSION:} + genshi pytest pytest-cov - From 03d08fcee69e2673f397d5a7296d09ad9e610464 Mon Sep 17 00:00:00 2001 From: Michael Farrell Date: Thu, 20 Jun 2019 14:53:03 +1000 Subject: [PATCH 11/20] Apply PR#27 (Windows locale fix) When the default locale cannot be detected, `locale.getdefaultlocale` may return `(None, None)`. This works around the issue. --- datetime_tz/detect_windows.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/datetime_tz/detect_windows.py b/datetime_tz/detect_windows.py index 3f708aa..b2fe6db 100644 --- a/datetime_tz/detect_windows.py +++ b/datetime_tz/detect_windows.py @@ -116,7 +116,12 @@ def _detect_timezone_windows(): win32timezone.TimeZoneInfo._get_indexed_time_zone_keys("Std")) win32tz_key_name = win32timezone_to_en.get(win32tz_name, win32tz_name) - territory = locale.getdefaultlocale()[0].split("_", 1)[1] + language_code = locale.getdefaultlocale()[0] + if language_code is None: + # Failure getting the system locale, result may be wrong! + territory = None + else: + territory = language_code.split("_", 1)[1] olson_name = win32tz_map.win32timezones.get((win32tz_key_name, territory), win32tz_map.win32timezones.get(win32tz_key_name, None)) if not olson_name: return None From af0b3a293599966dcf769438002864b1c4e66efb Mon Sep 17 00:00:00 2001 From: Michael Farrell Date: Thu, 20 Jun 2019 14:56:18 +1000 Subject: [PATCH 12/20] Plumb open args/kwargs This makes the mock functions work like the underlying builtin. --- tests.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tests.py b/tests.py index 3d72682..a3b3df7 100644 --- a/tests.py +++ b/tests.py @@ -254,30 +254,31 @@ def os_path_exists_fake(filename, os_path_exists=os.path.exists): self.mocked("os.path.exists", os_path_exists_fake) # Check that when /etc/timezone is a valid input - def timezone_valid_fake(filename, mode="r", open=open): + real_open = builtins.open + def timezone_valid_fake(filename, *args, **kw): if filename == "/etc/timezone": return StringIO("Australia/Sydney") - return open(filename, mode) + return real_open(filename, *args, **kw) self.mocked("builtins.open", timezone_valid_fake) tzinfo = datetime_tz._detect_timezone_etc_timezone() self.assertTimezoneEqual(tzinfo, pytz.timezone("Australia/Sydney")) # Check that when /etc/timezone is invalid timezone - def timezone_invalid_fake(filename, mode="r", open=open): + def timezone_invalid_fake(filename, *args, **kw): if filename == "/etc/timezone": return StringIO("Invalid-Timezone") - return open(filename, mode) + return real_open(filename, *args, **kw) self.mocked("builtins.open", timezone_invalid_fake) tzinfo = datetime_tz._detect_timezone_etc_timezone() self.assertEqual(None, tzinfo) # Check that when /etc/timezone is random "binary" data - def timezone_binary_fake(filename, mode="r", open=open): + def timezone_binary_fake(filename, *args, **kw): if filename == "/etc/timezone": return StringIO("\0\r\n\t\0\r\r\n\0") - return open(filename, mode) + return real_open(filename, *args, **kw) self.mocked("builtins.open", timezone_binary_fake) tzinfo = datetime_tz._detect_timezone_etc_timezone() @@ -316,7 +317,8 @@ def os_walk_fake(dirname, *args, **kw): return os_walk(dirname, *args, **kw) self.mocked("os.walk", os_walk_fake) - def localtime_valid_fake(filename, mode="r", open=open): + real_open = builtins.open + def localtime_valid_fake(filename, *args, **kw): if filename == "/etc/localtime": filename = os.path.join(os.path.dirname(__file__), localtime_file) @@ -331,8 +333,7 @@ def localtime_valid_fake(filename, mode="r", open=open): ): filename = os.path.join(os.path.dirname(__file__), "test_zonedata_utc") - return open(filename, mode) - self.mocked("builtins.open", localtime_valid_fake) + return real_open(filename, *args, **kw) self.assertEqual( ["Australia/Melbourne", "Australia/Sydney", "Etc/UTC"], From 182f5907a70bc50b0a9e6cb9074030b2803a7097 Mon Sep 17 00:00:00 2001 From: Michael Farrell Date: Thu, 20 Jun 2019 14:58:09 +1000 Subject: [PATCH 13/20] Fix assertEquals --- tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests.py b/tests.py index a3b3df7..9c8d659 100644 --- a/tests.py +++ b/tests.py @@ -1341,7 +1341,7 @@ def testDefaultTzinfos(self): self.assertTrue("Australia/Sydney" in def_tz.keys()) self.assertTrue(def_tz.has_key("Australia/Sydney")) self.assertRaises(KeyError, def_tz.get, "Made/Up") - self.assertEquals(def_tz.get("Made/Up", None), None) + self.assertEqual(def_tz.get("Made/Up", None), None) class datetime_tz_test_subclass(datetime_tz.datetime_tz): From 08c89b8f63669391bd625fca5d9019f120b20fae Mon Sep 17 00:00:00 2001 From: Michael Farrell Date: Thu, 20 Jun 2019 15:01:00 +1000 Subject: [PATCH 14/20] Fix missing mock --- tests.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests.py b/tests.py index 9c8d659..b790bfd 100644 --- a/tests.py +++ b/tests.py @@ -334,6 +334,7 @@ def localtime_valid_fake(filename, *args, **kw): filename = os.path.join(os.path.dirname(__file__), "test_zonedata_utc") return real_open(filename, *args, **kw) + self.mocked("builtins.open", localtime_valid_fake) self.assertEqual( ["Australia/Melbourne", "Australia/Sydney", "Etc/UTC"], From 0cf91693948af2faed77f9b84af3f365539e1410 Mon Sep 17 00:00:00 2001 From: Michael Farrell Date: Thu, 20 Jun 2019 15:52:46 +1000 Subject: [PATCH 15/20] Update README with correct links and badges. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6fd46ad..17c06ba 100644 --- a/README.md +++ b/README.md @@ -23,5 +23,5 @@ For development: [![Build Status](https://travis-ci.org/mithro/python-datetime-tz.png?branch=master)](https://travis-ci.org/mithro/python-datetime-tz) [![Coverage Status](https://coveralls.io/repos/mithro/python-datetime-tz/badge.png)](https://coveralls.io/r/mithro/python-datetime-tz) -[![PyPi Version](https://pypip.in/v/python-datetime-tz/badge.png)](https://crate.io/packages/python-coveralls/) -[![PyPi Downloads](https://pypip.in/d/python-datetime-tz/badge.png)](https://crate.io/packages/python-coveralls/) +[![PyPi Version](https://img.shields.io/pypi/v/python-datetime-tz.svg)](https://pypi.org/project/python-datetime-tz/) +[![PyPi Downloads](https://img.shields.io/pypi/dm/python-datetime-tz.svg)](https://pypi.org/project/python-datetime-tz/) From 437f0ca84036790f9a96e0a5e7c6751d6f9cfdc4 Mon Sep 17 00:00:00 2001 From: Michael Farrell Date: Thu, 20 Jun 2019 15:57:00 +1000 Subject: [PATCH 16/20] version++ --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ef23089..e4bb02b 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ def run(self): data = dict( name='python-datetime-tz', - version='0.5.3', + version='0.5.4', author='Tim Ansell', author_email='mithro@mithis.com', url='http://github.com/mithro/python-datetime-tz', From d793b7e88a1ec2b104150680fbc3c74b744484d9 Mon Sep 17 00:00:00 2001 From: Michael Farrell Date: Thu, 20 Jun 2019 16:06:30 +1000 Subject: [PATCH 17/20] Fix spelling mistake --- datetime_tz/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datetime_tz/__init__.py b/datetime_tz/__init__.py index 8e61295..9c388ee 100644 --- a/datetime_tz/__init__.py +++ b/datetime_tz/__init__.py @@ -217,7 +217,7 @@ def detect_timezone(): if tz is not None: return tz - # Next we try and use a similiar method to what PHP does. + # Next we try and use a similar method to what PHP does. # We first try to search on time.tzname, time.timezone, time.daylight to # match a pytz zone. warnings.warn("Had to fall back to worst detection method (the 'PHP' " From 5c55989893a8ddaff8ab350c10b3c27dec3c7b0c Mon Sep 17 00:00:00 2001 From: Matt Hampton Date: Fri, 20 Mar 2020 16:33:57 +0200 Subject: [PATCH 18/20] Changes introduced in https://bugs.python.org/issue32417 for python 3.8 changed the datetime arithmetic functions to return instances of the same class as the instance, instead of new datetime.datetime instances. See the "What's new notes": Arithmetic operations between subclasses of datetime.date or datetime.datetime and datetime.timedelta objects now return an instance of the subclass, rather than the base class. This also affects the return type of operations whose implementation (directly or indirectly) uses datetime.timedelta arithmetic, such as astimezone(). (Contributed by Paul Ganssle in bpo-32417.) Unfortunately, the side-effect of this change was to end up calling datetime_tz.__new__ with the set of arguments that result in tzinfo.localize being called on the result instead of tzinfo.normalize. This changed the resulting answer, in particular where DST transitions were involved. The testAroundDst were failing with python 3.8 before this change. The fix makes it behave in the same way as it did before - where a datetime.datetime object is return from the arithmetic result and then passed to the datetime_tz constructor. The other functions that do something similar work in the same way (e.g. astimezone). --- datetime_tz/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datetime_tz/__init__.py b/datetime_tz/__init__.py index 9c388ee..e939a56 100644 --- a/datetime_tz/__init__.py +++ b/datetime_tz/__init__.py @@ -866,7 +866,7 @@ def _wrap_method(name): # Have to give the second argument as method has no __module__ option. @functools.wraps(method, ("__name__", "__doc__"), ()) def wrapper(self, *args, **kw): - r = method(self, *args, **kw) + r = method(self.asdatetime(naive=False), *args, **kw) if isinstance(r, datetime.datetime) and not isinstance(r, type(self)): r = type(self)(r) From 7e839d822bf2ebe3bb42af352f8af2c0b0d8d4b4 Mon Sep 17 00:00:00 2001 From: Michael Farrell Date: Thu, 5 Nov 2020 10:46:39 +1100 Subject: [PATCH 19/20] Migrate from Genshi to defusedxml in update_win32tz_map.py: * Add `defusedxml` to `install_requires`. Genshi was never listed, although it was only a requirement for building sdist. * Removed support for copying comments from CLDR XML. This would be difficult without a DOM parser, but the source file contains a local DTD reference which is banned in defusedxml. * Switch `create_win32tz_map` to a mostly-functional processing model, by `yield`ing earlier, instead of building complete dicts and `yield`ing after-the-fact. This causes output to be no longer sorted alphabetically. * Move special handling of `territory=001` into `create_win32tz_map`. Tested that generated content is identical, although the ordering will differ. Google-internal bug: 160436149 --- README.md | 2 +- datetime_tz/update_win32tz_map.py | 92 ++++++++++++------------------- pyproject.toml | 2 +- requirements.txt | 4 +- setup.py | 6 +- tox.ini | 2 +- 6 files changed, 44 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index 17c06ba..58b1132 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ This module depends on: For development: * PyLint - Needed for checking for link. - * Genshi - Needed for building windows mapping file. + * defusedxml - Needed for building windows mapping file. [![Build Status](https://travis-ci.org/mithro/python-datetime-tz.png?branch=master)](https://travis-ci.org/mithro/python-datetime-tz) [![Coverage Status](https://coveralls.io/repos/mithro/python-datetime-tz/badge.png)](https://coveralls.io/r/mithro/python-datetime-tz) diff --git a/datetime_tz/update_win32tz_map.py b/datetime_tz/update_win32tz_map.py index 1111d78..371ec62 100644 --- a/datetime_tz/update_win32tz_map.py +++ b/datetime_tz/update_win32tz_map.py @@ -23,15 +23,7 @@ import hashlib import os -try: - import urllib.request as urllib2 -except ImportError: - import urllib2 - -try: - from io import StringIO -except ImportError: - from StringIO import StringIO +from defusedxml import ElementTree try: # pylint: disable=redefined-builtin @@ -39,12 +31,15 @@ except ImportError: pass -import genshi.input try: from datetime_tz import win32tz_map except ImportError: win32tz_map = None +try: + import urllib.request as urllib2 +except ImportError: + import urllib2 _CLDR_WINZONES_URL = "https://github.com/unicode-org/cldr/raw/master/common/supplemental/windowsZones.xml" # pylint: disable=line-too-long @@ -61,35 +56,27 @@ def create_win32tz_map(windows_zones_xml): windows_zones_xml: The CLDR XML mapping. Yields: - (win32_name, olson_name, comment) + For "default" territory (001): (win32_name, olson_name) + Where territory is set: ((win32_name, territory), olson_name) """ - coming_comment = None - win32_name = None - territory = None - parser = genshi.input.XMLParser(StringIO(windows_zones_xml)) - map_zones = {} - zone_comments = {} - - for kind, data, _ in parser: - if kind == genshi.core.START and str(data[0]) == "mapZone": - attrs = data[1] - win32_name, territory, olson_name = ( - attrs.get("other"), attrs.get("territory"), attrs.get("type").split(" ")[0]) - - map_zones[(win32_name, territory)] = olson_name - elif kind == genshi.core.END and str(data) == "mapZone" and win32_name: - if coming_comment: - zone_comments[(win32_name, territory)] = coming_comment - coming_comment = None - win32_name = None - elif kind == genshi.core.COMMENT: - coming_comment = data.strip() - elif kind in (genshi.core.START, genshi.core.END, genshi.core.COMMENT): - coming_comment = None - - for win32_name, territory in sorted(map_zones): - yield (win32_name, territory, map_zones[(win32_name, territory)], - zone_comments.get((win32_name, territory), None)) + parser = ElementTree.fromstring(windows_zones_xml) + map_timezones = parser.find("windowsZones").find("mapTimezones") + + for child in map_timezones: + if child.tag == "mapZone": + win32_name = str(child.attrib.get("other", "")) + territory = str(child.attrib.get("territory", "")) + # Some `type` parameters are have multiple values, separated by spaces + # eg: "America/Denver America/Boise" + olson_name = str(child.attrib.get("type", "")).split(" ")[0] + + if not win32_name or not olson_name: + continue + + if territory == "001" or not territory: + yield (win32_name, olson_name) + else: + yield ((win32_name, territory), olson_name) def update_stored_win32tz_map(): @@ -97,9 +84,6 @@ def update_stored_win32tz_map(): windows_zones_xml = download_cldr_win32tz_map_xml() source_hash = hashlib.md5(windows_zones_xml).hexdigest() - if hasattr(windows_zones_xml, "decode"): - windows_zones_xml = windows_zones_xml.decode("utf-8") - map_zones = create_win32tz_map(windows_zones_xml) map_dir = os.path.dirname(os.path.abspath(__file__)) map_filename = os.path.join(map_dir, "win32tz_map.py") @@ -110,24 +94,16 @@ def update_stored_win32tz_map(): return False map_file = open(map_filename, "w") + map_file.write(( + "'''Map between Windows and Olson timezones taken from {0}\n" + "Generated automatically by {1}'''\n" + "source_hash = {2!r} # md5 sum of xml source data\n" + "win32timezones = {{\n" + ).format(_CLDR_WINZONES_URL, __file__, source_hash)) + + for z in map_zones: + map_file.write(" %r: %r,\n" % z) - comment = "Map between Windows and Olson timezones taken from %s" % ( - _CLDR_WINZONES_URL,) - comment2 = "Generated automatically from datetime_tz.py" - map_file.write("'''%s\n" % comment) - map_file.write("%s'''\n" % comment2) - - map_file.write("source_hash = '%s' # md5 sum of xml source data\n" % ( - source_hash)) - - map_file.write("win32timezones = {\n") - for win32_name, territory, olson_name, comment in map_zones: - if territory == '001': - map_file.write(" %r: %r, # %s\n" % ( - str(win32_name), str(olson_name), comment or "")) - else: - map_file.write(" %r: %r, # %s\n" % ( - (str(win32_name), str(territory)), str(olson_name), comment or "")) map_file.write("}\n") map_file.close() diff --git a/pyproject.toml b/pyproject.toml index dff4a2b..cc8a7b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ requires = [ "setuptools>=41.0", "wheel", - "Genshi", + "defusedxml", "python-dateutil>=2.0", "pytz >= 2011g", ] diff --git a/requirements.txt b/requirements.txt index cb8df59..63e0490 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -Genshi +defusedxml python-dateutil -pytz>=2007g +pytz>=2011g diff --git a/setup.py b/setup.py index e4bb02b..5816dd6 100644 --- a/setup.py +++ b/setup.py @@ -59,7 +59,11 @@ def run(self): "Topic :: Software Development :: Internationalization", ], packages=['datetime_tz'], - install_requires=["pytz >= 2011g", "python-dateutil >= 2.0"], + install_requires=[ + "defusedxml", + "python-dateutil >= 2.0", + "pytz >= 2011g", + ], py_modules=['datetime_tz','datetime_tz.pytz_abbr'], test_suite='tests', cmdclass={'sdist': update_sdist, "install": update_install}, diff --git a/tox.ini b/tox.ini index 54dec6c..aefa0a7 100644 --- a/tox.ini +++ b/tox.ini @@ -12,6 +12,6 @@ commands = pytest {posargs: --cov=datetime_tz} tests.py deps = python-dateutil pytz{env:PYTZ_VERSION:} - genshi + defusedxml pytest pytest-cov From 91dfd676b4340045d5acf62dcbb5aff235d624fc Mon Sep 17 00:00:00 2001 From: Michael Farrell Date: Thu, 5 Nov 2020 11:11:06 +1100 Subject: [PATCH 20/20] Run test suite with Python 3.8 (thanks to #28) --- .travis.yml | 8 +++----- tox.ini | 3 ++- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9bbbb95..5f83dcd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,14 +2,12 @@ language: python python: - 2.7 - 3.6 + - 3.7 + - 3.8 - nightly +dist: xenial matrix: - include: - # Required to run Python 3.7 - - python: 3.7 - dist: xenial - sudo: required allow_failures: - python: nightly diff --git a/tox.ini b/tox.ini index aefa0a7..b9d1a9e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,8 @@ [tox] envlist = py27, py36, - py37 + py37, + py38 minversion = 3.3.0 skip_missing_interpreters = true isolated_build = true