From 8d55b834458bc4ca61d6e437a955b7fe1c4f2f39 Mon Sep 17 00:00:00 2001 From: Chris Down Date: Mon, 2 Sep 2019 14:56:52 +0100 Subject: [PATCH 01/13] Increase HTTP timeout to 30s See discussion in #32. --- tzupdate.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tzupdate.py b/tzupdate.py index 3d29f69..4c6b8ae 100755 --- a/tzupdate.py +++ b/tzupdate.py @@ -26,6 +26,7 @@ DEFAULT_ZONEINFO_PATH = "/usr/share/zoneinfo" DEFAULT_LOCALTIME_PATH = "/etc/localtime" DEFAULT_DEBIAN_TIMEZONE_PATH = "/etc/timezone" +DEFAULT_HTTP_TIMEOUT = 30.0 # url: A url with an "ip" key to be replaced with an optional IP @@ -50,7 +51,7 @@ def get_deep(item, keys): return tmp -def get_timezone(ip, timeout=5.0, services=SERVICES): +def get_timezone(ip, timeout=DEFAULT_HTTP_TIMEOUT, services=SERVICES): q = Queue() threads = [ @@ -242,7 +243,7 @@ def parse_args(argv): help="maximum number of seconds to wait for APIs to return (default: " "%(default)s)", type=float, - default=5.0, + default=DEFAULT_HTTP_TIMEOUT, ) parser.add_argument( "--debug", From 22fa40fef3873b1e30e9f8807722f53dd266612a Mon Sep 17 00:00:00 2001 From: Chris Down Date: Wed, 11 Sep 2019 17:47:04 +0100 Subject: [PATCH 02/13] Don't query API unnecessarily Wait, what? How did this happen? :| --- tzupdate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tzupdate.py b/tzupdate.py index 4c6b8ae..6753034 100755 --- a/tzupdate.py +++ b/tzupdate.py @@ -82,7 +82,7 @@ def get_timezone_for_ip(ip, service, queue_obj): log.warning("%s returned %d, ignoring", api_url, api_response_obj.status_code) return - api_response = requests.get(api_url).json() + api_response = api_response_obj.json() log.debug("API response from %s: %r", api_url, api_response) try: From 7044e18a079eebf0d76cb96275c6d90ee256e4a9 Mon Sep 17 00:00:00 2001 From: Chris Down Date: Wed, 11 Sep 2019 17:52:37 +0100 Subject: [PATCH 03/13] Remove dependency on requests --- MANIFEST.in | 1 - requirements.txt | 1 - setup.py | 4 ---- tests/_test_utils.py | 6 +++++- tox.ini | 1 - tzupdate.py | 18 ++++++++++++------ 6 files changed, 17 insertions(+), 14 deletions(-) delete mode 100644 requirements.txt diff --git a/MANIFEST.in b/MANIFEST.in index d71c874..24e14c8 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,6 @@ include LICENSE include MANIFEST.in include README.rst -include requirements.txt include .coveragerc recursive-include * requirements.txt diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index f229360..0000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -requests diff --git a/setup.py b/setup.py index 1f2672f..5c96b2b 100755 --- a/setup.py +++ b/setup.py @@ -6,9 +6,6 @@ with open("README.rst") as readme_f: README = readme_f.read() -with open("requirements.txt") as requirements_f: - REQUIREMENTS = requirements_f.readlines() - setup( name="tzupdate", version="1.5.0", @@ -19,7 +16,6 @@ author="Chris Down", author_email="chris@chrisdown.name", py_modules=["tzupdate"], - install_requires=REQUIREMENTS, entry_points={"console_scripts": ["tzupdate=tzupdate:main"]}, keywords="timezone localtime tz", classifiers=[ diff --git a/tests/_test_utils.py b/tests/_test_utils.py index 81d7667..f8445ae 100644 --- a/tests/_test_utils.py +++ b/tests/_test_utils.py @@ -7,10 +7,14 @@ import ipaddress from hypothesis.strategies import integers, builds +try: + _ipa_type = unicode # pytype: disable=name-error +except NameError: + _ipa_type = str IP_ADDRESSES = builds( ipaddress.IPv4Address, integers(min_value=0, max_value=(2 ** 32 - 1)) -).map(str) +).map(_ipa_type) FAKE_TIMEZONE = "Fake/Timezone" FAKE_ZONEINFO_PATH = "/path/to/zoneinfo" diff --git a/tox.ini b/tox.ini index f91f5f3..ff9eaf3 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,6 @@ envlist = system [testenv] basepython = python deps = - -rrequirements.txt -rtests/requirements.txt commands = nosetests -q diff --git a/tzupdate.py b/tzupdate.py index 6753034..0d6f377 100755 --- a/tzupdate.py +++ b/tzupdate.py @@ -11,15 +11,17 @@ import collections import errno import logging +import json import os import sys -import requests - try: from queue import Empty + from urllib.request import urlopen + from urllib.error import HTTPError except ImportError: # Python 2 fallback from Queue import Empty + from urllib2 import urlopen, HTTPError log = logging.getLogger(__name__) @@ -76,13 +78,17 @@ def get_timezone(ip, timeout=DEFAULT_HTTP_TIMEOUT, services=SERVICES): def get_timezone_for_ip(ip, service, queue_obj): api_url = service.url.format(ip=ip or "") - api_response_obj = requests.get(api_url) - if not api_response_obj.ok: - log.warning("%s returned %d, ignoring", api_url, api_response_obj.status_code) + try: + # The caller is responsible for providing a service string which + # doesn't permit walking file: URIs or whatever, so silence bandit's + # warning about that + api_response_obj = urlopen(api_url) # nosec + except HTTPError as thrown_exc: + log.warning("%s returned %d, ignoring", api_url, thrown_exc.code) return - api_response = api_response_obj.json() + api_response = json.loads(api_response_obj.read().decode("utf8")) log.debug("API response from %s: %r", api_url, api_response) try: From 6ceeb5a3df61d31bf16e8026df1c58d78bb77aac Mon Sep 17 00:00:00 2001 From: Chris Down Date: Wed, 11 Sep 2019 19:04:00 +0100 Subject: [PATCH 04/13] requirements: Pin to major versions (or minor for <1) --- tests/requirements.txt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/requirements.txt b/tests/requirements.txt index 01b8117..df53e59 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,6 +1,6 @@ -nose -hypothesis -httpretty -parameterized -mock -ipaddress +nose>=1,<2 +hypothesis>=4,<5 +httpretty>=0.9,<0.10 +parameterized>=0.7,<0.8 +mock>=3,<4 +ipaddress>=1,<2 From 3895bca2b5d52ba92f9597e1330c56cc5b901cef Mon Sep 17 00:00:00 2001 From: Chris Down Date: Wed, 11 Sep 2019 22:21:09 +0100 Subject: [PATCH 05/13] setup.py: Add more trove classifiers --- setup.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/setup.py b/setup.py index 5c96b2b..6aa9922 100755 --- a/setup.py +++ b/setup.py @@ -21,8 +21,13 @@ classifiers=[ "Development Status :: 5 - Production/Stable", "License :: Public Domain", + "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", "Topic :: System", "Topic :: System :: Networking :: Time Synchronization", "Topic :: Utilities", From c2d45c90f684ecd58f2130a4973e24894772ed95 Mon Sep 17 00:00:00 2001 From: Chris Down Date: Fri, 3 Jan 2020 11:39:12 +0000 Subject: [PATCH 06/13] Mention --timeout on timeout --- tzupdate.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tzupdate.py b/tzupdate.py index 0d6f377..4c9e3d4 100755 --- a/tzupdate.py +++ b/tzupdate.py @@ -67,8 +67,9 @@ def get_timezone(ip, timeout=DEFAULT_HTTP_TIMEOUT, services=SERVICES): timezone = q.get(block=True, timeout=timeout) except Empty: raise TimezoneAcquisitionError( - "No usable response from any API in {} seconds".format(timeout) - ) + "No usable response from any API in {} seconds. Consider " + "increasing --timeout if your connection is slow.".format(timeout) + ) from None finally: for t in threads: t.terminate() From 63e0e8df2d940218e2936ba51579801547fc6fb0 Mon Sep 17 00:00:00 2001 From: Chris Down Date: Fri, 3 Jan 2020 11:51:29 +0000 Subject: [PATCH 07/13] testing infra: Update from srt --- .coveragerc | 5 -- .pylintrc | 2 - .travis.yml | 40 +++++++++---- tests/requirements.txt | 3 +- tests/{e2e_tests.py => test_e2e.py} | 12 ++-- tests/{unit_tests.py => test_unit.py} | 82 +++++++++++++++------------ tox.ini | 24 +++++--- 7 files changed, 99 insertions(+), 69 deletions(-) delete mode 100644 .coveragerc delete mode 100644 .pylintrc rename tests/{e2e_tests.py => test_e2e.py} (91%) rename tests/{unit_tests.py => test_unit.py} (76%) diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 6c5ba44..0000000 --- a/.coveragerc +++ /dev/null @@ -1,5 +0,0 @@ -[report] -exclude_lines = - except ImportError - if argv is None - if __name__ == .__main__.: diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index 7321ae6..0000000 --- a/.pylintrc +++ /dev/null @@ -1,2 +0,0 @@ -[MESSAGES CONTROL] -disable=invalid-name,missing-docstring diff --git a/.travis.yml b/.travis.yml index 3ccfb97..852aef6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,19 +1,39 @@ dist: xenial language: python cache: pip +install: pip install --upgrade tox +script: tox # Drop once EOL: https://devguide.python.org/#status-of-python-branches -python: - - '2.7' - - '3.5' - - '3.6' +jobs: + include: + ## Linux -install: pip install tox -script: - - 'tox' + # CPython (in official virtualenv) + - python: '3.5' + - python: '3.6' + # 3.7 is below + + # PyPy (in official virtualenv) + - python: pypy3 + env: TOXENV=pypy3 + + # Correctness tests. "coverage" toxenv runs tests, so no need to run + # TOXENV=py38. + # + # TODO: pytype doesn't yet support 3.8, switch this with 3.8 above when it + # does -matrix: - include: - # "coverage" toxenv runs tests, so no need to run TOXENV=py37 - python: '3.7' env: TOXENV=black,pylint,pytype,bandit,coverage + + ## Special jobs + + # Run long Hypothesis tests for release/cron + - if: branch =~ ^release/.*$ or type = cron + python: '3.8' + env: TOXENV=py-release + +notifications: + email: + - travis+tzupdate@chrisdown.name diff --git a/tests/requirements.txt b/tests/requirements.txt index df53e59..a644964 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,4 +1,5 @@ -nose>=1,<2 +pytest>=4,<5 +pytest-cov>=2,<3 hypothesis>=4,<5 httpretty>=0.9,<0.10 parameterized>=0.7,<0.8 diff --git a/tests/e2e_tests.py b/tests/test_e2e.py similarity index 91% rename from tests/e2e_tests.py rename to tests/test_e2e.py index 73feb83..cd876f2 100644 --- a/tests/e2e_tests.py +++ b/tests/test_e2e.py @@ -3,7 +3,7 @@ import httpretty import tzupdate import mock -from nose.tools import assert_false, assert_raises +import pytest from tests._test_utils import FAKE_SERVICES, FAKE_TIMEZONE, setup_basic_api_response @@ -29,8 +29,8 @@ def test_print_only_no_link(link_localtime_mock, deb_tz_mock): setup_basic_api_response() args = ["-p"] tzupdate.main(args, services=FAKE_SERVICES) - assert_false(link_localtime_mock.called) - assert_false(deb_tz_mock.called) + assert not link_localtime_mock.called + assert not deb_tz_mock.called @mock.patch("tzupdate.write_debian_timezone") @@ -38,8 +38,8 @@ def test_print_only_no_link(link_localtime_mock, deb_tz_mock): def test_print_sys_tz_no_link(link_localtime_mock, deb_tz_mock): args = ["--print-system-timezone"] tzupdate.main(args, services=FAKE_SERVICES) - assert_false(link_localtime_mock.called) - assert_false(deb_tz_mock.called) + assert not link_localtime_mock.called + assert not deb_tz_mock.called @httpretty.activate @@ -91,5 +91,5 @@ def test_timeout_results_in_exception(process_mock): # should time out setup_basic_api_response() args = ["-s", "0.01"] - with assert_raises(tzupdate.TimezoneAcquisitionError): + with pytest.raises(tzupdate.TimezoneAcquisitionError): tzupdate.main(args, services=FAKE_SERVICES) diff --git a/tests/unit_tests.py b/tests/test_unit.py similarity index 76% rename from tests/unit_tests.py rename to tests/test_unit.py index 1688c0c..7966c25 100644 --- a/tests/unit_tests.py +++ b/tests/test_unit.py @@ -5,6 +5,7 @@ import os import errno import mock +import pytest from tests._test_utils import ( IP_ADDRESSES, FAKE_SERVICES, @@ -12,32 +13,29 @@ FAKE_ZONEINFO_PATH, setup_basic_api_response, ) -from nose.tools import ( - assert_raises, - eq_ as eq, - assert_true, - assert_is_none, - assert_in, - assert_false, -) -from parameterized import parameterized -from hypothesis import given, settings +from hypothesis import given, settings, HealthCheck, assume from hypothesis.strategies import sampled_from, none, one_of, text, integers ERROR_STATUSES = [s for s in httpretty.http.STATUSES if 400 <= s <= 599] +SUPPRESSED_CHECKS = [HealthCheck.too_slow] + +settings.register_profile("base", settings(suppress_health_check=SUPPRESSED_CHECKS)) +settings.register_profile( + "release", settings(max_examples=1000, suppress_health_check=SUPPRESSED_CHECKS) +) +settings.load_profile(os.getenv("HYPOTHESIS_PROFILE", "base")) @httpretty.activate @given(one_of(IP_ADDRESSES, none()), sampled_from(FAKE_SERVICES)) -@settings(max_examples=20) def test_get_timezone_for_ip(ip, service): fake_queue = mock.Mock() setup_basic_api_response() tzupdate.get_timezone_for_ip(ip=ip, service=service, queue_obj=fake_queue) if ip is not None: - assert_in(ip, httpretty.last_request().path) + assert ip in httpretty.last_request().path fake_queue.put.assert_called_once_with(FAKE_TIMEZONE) @@ -51,23 +49,23 @@ def test_get_sys_timezone(): @httpretty.activate @given(one_of(IP_ADDRESSES, none()), sampled_from(FAKE_SERVICES)) -@settings(max_examples=20) def test_get_timezone_for_ip_empty_resp(ip, service): fake_queue = mock.Mock() setup_basic_api_response(empty_resp=True) - assert_is_none( + assert ( tzupdate.get_timezone_for_ip(ip=ip, service=service, queue_obj=fake_queue) + is None ) @httpretty.activate @given(one_of(IP_ADDRESSES, none()), sampled_from(FAKE_SERVICES)) -@settings(max_examples=20) def test_get_timezone_for_ip_empty_val(ip, service): fake_queue = mock.Mock() setup_basic_api_response(empty_val=True) - assert_is_none( + assert ( tzupdate.get_timezone_for_ip(ip=ip, service=service, queue_obj=fake_queue) + is None ) @@ -77,12 +75,12 @@ def test_get_timezone_for_ip_empty_val(ip, service): sampled_from(FAKE_SERVICES), sampled_from(ERROR_STATUSES), ) -@settings(max_examples=20) def test_get_timezone_for_ip_doesnt_raise(ip, service, status): fake_queue = mock.Mock() setup_basic_api_response(status=status) - assert_is_none( + assert ( tzupdate.get_timezone_for_ip(ip=ip, service=service, queue_obj=fake_queue) + is None ) @@ -97,17 +95,31 @@ def test_link_localtime(isfile_mock, symlink_mock, unlink_mock): FAKE_TIMEZONE, tzupdate.DEFAULT_ZONEINFO_PATH, tzupdate.DEFAULT_LOCALTIME_PATH ) - assert_true(unlink_mock.called_once_with([expected])) - assert_true( - symlink_mock.called_once_with([expected, tzupdate.DEFAULT_LOCALTIME_PATH]) - ) + assert unlink_mock.called_once_with([expected]) + assert symlink_mock.called_once_with([expected, tzupdate.DEFAULT_LOCALTIME_PATH]) + + assert zoneinfo_tz_path == expected + + +@given(text()) +def test_link_localtime_traversal_attack_root(questionable_timezone): + assume(tzupdate.DEFAULT_ZONEINFO_PATH not in questionable_timezone) + questionable_timezone = "/" + questionable_timezone + + with pytest.raises(tzupdate.DirectoryTraversalError): + tzupdate.link_localtime( + questionable_timezone, + tzupdate.DEFAULT_ZONEINFO_PATH, + tzupdate.DEFAULT_LOCALTIME_PATH, + ) - eq(zoneinfo_tz_path, expected) +@given(text()) +def test_link_localtime_traversal_attack_dotdot(questionable_timezone): + assume(tzupdate.DEFAULT_ZONEINFO_PATH not in questionable_timezone) + questionable_timezone = "../../../" + questionable_timezone -@parameterized(["/foo/bar", "../../../../foo/bar"]) -def test_link_localtime_traversal_attack(questionable_timezone): - with assert_raises(tzupdate.DirectoryTraversalError): + with pytest.raises(tzupdate.DirectoryTraversalError): tzupdate.link_localtime( questionable_timezone, tzupdate.DEFAULT_ZONEINFO_PATH, @@ -118,7 +130,7 @@ def test_link_localtime_traversal_attack(questionable_timezone): @mock.patch("tzupdate.os.path.isfile") def test_link_localtime_timezone_not_available(isfile_mock): isfile_mock.return_value = False - with assert_raises(tzupdate.TimezoneNotLocallyAvailableError): + with pytest.raises(tzupdate.TimezoneNotLocallyAvailableError): tzupdate.link_localtime( FAKE_TIMEZONE, tzupdate.DEFAULT_ZONEINFO_PATH, @@ -131,14 +143,14 @@ def test_link_localtime_timezone_not_available(isfile_mock): def test_link_localtime_permission_denied(isfile_mock, unlink_mock): isfile_mock.return_value = True unlink_mock.side_effect = OSError(errno.EACCES, "Permission denied yo") - with assert_raises(OSError) as raise_cm: + with pytest.raises(OSError) as raise_cm: tzupdate.link_localtime( FAKE_TIMEZONE, tzupdate.DEFAULT_ZONEINFO_PATH, tzupdate.DEFAULT_LOCALTIME_PATH, ) - eq(raise_cm.exception.errno, errno.EACCES) + assert raise_cm.value.errno == errno.EACCES @mock.patch("tzupdate.os.unlink") @@ -148,14 +160,14 @@ def test_link_localtime_oserror_not_permission(isfile_mock, unlink_mock): code = errno.ENOSPC unlink_mock.side_effect = OSError(code, "No space yo") - with assert_raises(OSError) as thrown_exc: + with pytest.raises(OSError) as thrown_exc: tzupdate.link_localtime( FAKE_TIMEZONE, tzupdate.DEFAULT_ZONEINFO_PATH, tzupdate.DEFAULT_LOCALTIME_PATH, ) - eq(thrown_exc.exception.errno, code) + assert thrown_exc.value.errno == code @mock.patch("tzupdate.os.unlink") @@ -175,7 +187,6 @@ def test_link_localtime_localtime_missing_no_raise( @given(text(), text()) -@settings(max_examples=20) def test_debian_tz_path_exists_not_forced(timezone, tz_path): mo = mock.mock_open() with mock.patch("tzupdate.open", mo, create=True): @@ -186,7 +197,6 @@ def test_debian_tz_path_exists_not_forced(timezone, tz_path): @given(text(), text()) -@settings(max_examples=20) def test_debian_tz_path_doesnt_exist_not_forced(timezone, tz_path): mo = mock.mock_open() mo.side_effect = OSError(errno.ENOENT, "") @@ -196,19 +206,17 @@ def test_debian_tz_path_doesnt_exist_not_forced(timezone, tz_path): @given(text(), text()) -@settings(max_examples=20) def test_debian_tz_path_other_error_raises(timezone, tz_path): mo = mock.mock_open() code = errno.EPERM mo.side_effect = OSError(code, "") with mock.patch("tzupdate.open", mo, create=True): - with assert_raises(OSError) as thrown_exc: + with pytest.raises(OSError) as thrown_exc: tzupdate.write_debian_timezone(timezone, tz_path, must_exist=True) - eq(thrown_exc.exception.errno, code) + assert thrown_exc.value.errno == code @given(text(), text()) -@settings(max_examples=20) def test_debian_tz_path_doesnt_exist_forced(timezone, tz_path): mo = mock.mock_open() with mock.patch("tzupdate.open", mo, create=True): diff --git a/tox.ini b/tox.ini index ff9eaf3..85f8a83 100644 --- a/tox.ini +++ b/tox.ini @@ -1,14 +1,14 @@ [tox] -envlist = system +envlist = py [testenv] -basepython = python deps = -rtests/requirements.txt commands = - nosetests -q - -[testenv:system] + {basepython} --version + pytest +setenv= + release: HYPOTHESIS_PROFILE=release [testenv:coverage] passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH @@ -17,7 +17,7 @@ deps = coveralls commands = coverage erase - nosetests -v --with-coverage --cover-branches --cover-package=tzupdate + pytest --cov=tzupdate --cov-branch --cov-fail-under=100 coveralls [testenv:pylint] @@ -26,10 +26,12 @@ deps = {[testenv]deps} pylint commands = - pylint tzupdate.py + # C0330: https://github.com/psf/black/issues/1178 + pylint --disable=C0330 tzupdate.py [testenv:black] skipsdist = True +whitelist_externals = sh deps = black commands = @@ -41,7 +43,7 @@ deps = {[testenv]deps} pytype commands = - pytype -d import-error . + pytype . [testenv:bandit] skipsdist = True @@ -50,3 +52,9 @@ deps = bandit commands = bandit tzupdate.py + +[testenv:pypy] +basepython = pypy + +[testenv:pypy3] +basepython = pypy3 From cd3a521244c90896dde35e8fde4d47b5f78051cc Mon Sep 17 00:00:00 2001 From: Chris Down Date: Fri, 3 Jan 2020 12:02:32 +0000 Subject: [PATCH 08/13] Drop py2 support --- tzupdate.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tzupdate.py b/tzupdate.py index 4c9e3d4..c127a75 100755 --- a/tzupdate.py +++ b/tzupdate.py @@ -15,13 +15,9 @@ import os import sys -try: - from queue import Empty - from urllib.request import urlopen - from urllib.error import HTTPError -except ImportError: # Python 2 fallback - from Queue import Empty - from urllib2 import urlopen, HTTPError +from queue import Empty +from urllib.request import urlopen +from urllib.error import HTTPError log = logging.getLogger(__name__) From dfa2d8a73c5fc6d49053f6a602244b1794842488 Mon Sep 17 00:00:00 2001 From: Chris Down Date: Fri, 3 Jan 2020 12:03:09 +0000 Subject: [PATCH 09/13] pylint: Disable C0116 --- tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 85f8a83..19851b9 100644 --- a/tox.ini +++ b/tox.ini @@ -27,7 +27,8 @@ deps = pylint commands = # C0330: https://github.com/psf/black/issues/1178 - pylint --disable=C0330 tzupdate.py + # C0116: This isn't a library, docstrings may or may not be there + pylint --disable=C0330,C0116 tzupdate.py [testenv:black] skipsdist = True From 2878aedd159dbc397dcf590db4b2ef9ccaf4bd7f Mon Sep 17 00:00:00 2001 From: Chris Down Date: Fri, 3 Jan 2020 12:08:47 +0000 Subject: [PATCH 10/13] Appease the pylint gods --- tests/test_unit.py | 8 ++++---- tzupdate.py | 41 +++++++++++++++++++++-------------------- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/tests/test_unit.py b/tests/test_unit.py index 7966c25..da3b3e0 100644 --- a/tests/test_unit.py +++ b/tests/test_unit.py @@ -32,7 +32,7 @@ def test_get_timezone_for_ip(ip, service): fake_queue = mock.Mock() setup_basic_api_response() - tzupdate.get_timezone_for_ip(ip=ip, service=service, queue_obj=fake_queue) + tzupdate.get_timezone_for_ip(ip_addr=ip, service=service, queue_obj=fake_queue) if ip is not None: assert ip in httpretty.last_request().path @@ -53,7 +53,7 @@ def test_get_timezone_for_ip_empty_resp(ip, service): fake_queue = mock.Mock() setup_basic_api_response(empty_resp=True) assert ( - tzupdate.get_timezone_for_ip(ip=ip, service=service, queue_obj=fake_queue) + tzupdate.get_timezone_for_ip(ip_addr=ip, service=service, queue_obj=fake_queue) is None ) @@ -64,7 +64,7 @@ def test_get_timezone_for_ip_empty_val(ip, service): fake_queue = mock.Mock() setup_basic_api_response(empty_val=True) assert ( - tzupdate.get_timezone_for_ip(ip=ip, service=service, queue_obj=fake_queue) + tzupdate.get_timezone_for_ip(ip_addr=ip, service=service, queue_obj=fake_queue) is None ) @@ -79,7 +79,7 @@ def test_get_timezone_for_ip_doesnt_raise(ip, service, status): fake_queue = mock.Mock() setup_basic_api_response(status=status) assert ( - tzupdate.get_timezone_for_ip(ip=ip, service=service, queue_obj=fake_queue) + tzupdate.get_timezone_for_ip(ip_addr=ip, service=service, queue_obj=fake_queue) is None ) diff --git a/tzupdate.py b/tzupdate.py index c127a75..6926919 100755 --- a/tzupdate.py +++ b/tzupdate.py @@ -19,7 +19,7 @@ from urllib.request import urlopen from urllib.error import HTTPError -log = logging.getLogger(__name__) +LOG = logging.getLogger(__name__) DEFAULT_ZONEINFO_PATH = "/usr/share/zoneinfo" DEFAULT_LOCALTIME_PATH = "/etc/localtime" @@ -49,32 +49,33 @@ def get_deep(item, keys): return tmp -def get_timezone(ip, timeout=DEFAULT_HTTP_TIMEOUT, services=SERVICES): - q = Queue() +def get_timezone(ip_addr, timeout=DEFAULT_HTTP_TIMEOUT, services=SERVICES): + api_resp_queue = Queue() threads = [ - Process(target=get_timezone_for_ip, args=(ip, svc, q)) for svc in services + Process(target=get_timezone_for_ip, args=(ip_addr, svc, api_resp_queue)) + for svc in services ] - for t in threads: - t.start() + for thread in threads: + thread.start() try: - timezone = q.get(block=True, timeout=timeout) + timezone = api_resp_queue.get(block=True, timeout=timeout) except Empty: raise TimezoneAcquisitionError( "No usable response from any API in {} seconds. Consider " "increasing --timeout if your connection is slow.".format(timeout) ) from None finally: - for t in threads: - t.terminate() + for thread in threads: + thread.terminate() return timezone -def get_timezone_for_ip(ip, service, queue_obj): - api_url = service.url.format(ip=ip or "") +def get_timezone_for_ip(ip_addr, service, queue_obj): + api_url = service.url.format(ip=ip_addr or "") try: # The caller is responsible for providing a service string which @@ -82,15 +83,15 @@ def get_timezone_for_ip(ip, service, queue_obj): # warning about that api_response_obj = urlopen(api_url) # nosec except HTTPError as thrown_exc: - log.warning("%s returned %d, ignoring", api_url, thrown_exc.code) + LOG.warning("%s returned %d, ignoring", api_url, thrown_exc.code) return api_response = json.loads(api_response_obj.read().decode("utf8")) - log.debug("API response from %s: %r", api_url, api_response) + LOG.debug("API response from %s: %r", api_url, api_response) try: - tz = get_deep(api_response, service.tz_keys) - if not tz: + timezone = get_deep(api_response, service.tz_keys) + if not timezone: raise KeyError except KeyError: msg = "Unspecified API error for {}.".format(service.url) @@ -99,9 +100,9 @@ def get_timezone_for_ip(ip, service, queue_obj): msg = get_deep(api_response, service.error_keys) except KeyError: pass - log.warning("%s failed: %s", api_url, msg) + LOG.warning("%s failed: %s", api_url, msg) else: - queue_obj.put(tz) + queue_obj.put(timezone) def write_debian_timezone(timezone, debian_timezone_path, must_exist=True): @@ -143,9 +144,9 @@ def check_directory_traversal(base_dir, requested_path): shares a common prefix with the absolute path of the requested zoneinfo file. """ - log.debug("Checking for traversal in path %s", requested_path) + LOG.debug("Checking for traversal in path %s", requested_path) requested_path_abs = os.path.abspath(requested_path) - log.debug("Absolute path of requested path is %s", requested_path_abs) + LOG.debug("Absolute path of requested path is %s", requested_path_abs) if os.path.commonprefix([base_dir, requested_path_abs]) != base_dir: raise DirectoryTraversalError( "%r (%r) is outside base directory %r, refusing to run" @@ -278,7 +279,7 @@ def main(argv=None, services=SERVICES): if args.timezone: timezone = args.timezone - log.debug("Using explicitly passed timezone: %s", timezone) + LOG.debug("Using explicitly passed timezone: %s", timezone) else: timezone = get_timezone(args.ip, timeout=args.timeout, services=services) From 824ff8da8407d6f3fe60ed4afc8a98bf33fbd95b Mon Sep 17 00:00:00 2001 From: Chris Down Date: Fri, 3 Jan 2020 12:12:03 +0000 Subject: [PATCH 11/13] coverage: Don't try to cover __main__ bits --- tox.ini | 2 +- tzupdate.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 19851b9..7afc3ca 100644 --- a/tox.ini +++ b/tox.ini @@ -17,7 +17,7 @@ deps = coveralls commands = coverage erase - pytest --cov=tzupdate --cov-branch --cov-fail-under=100 + pytest --cov=tzupdate --cov-branch --cov-fail-under=100 --cov-report term-missing coveralls [testenv:pylint] diff --git a/tzupdate.py b/tzupdate.py index 6926919..c152bdb 100755 --- a/tzupdate.py +++ b/tzupdate.py @@ -262,7 +262,7 @@ def parse_args(argv): def main(argv=None, services=SERVICES): - if argv is None: + if argv is None: # pragma: no cover argv = sys.argv[1:] args = parse_args(argv) @@ -318,5 +318,5 @@ class TimezoneAcquisitionError(TimezoneUpdateException): """ -if __name__ == "__main__": +if __name__ == "__main__": # pragma: no cover main() From 1813dd6b188c3116b224c8c4202d58281996bd9e Mon Sep 17 00:00:00 2001 From: Chris Down Date: Fri, 3 Jan 2020 12:13:12 +0000 Subject: [PATCH 12/13] travis: Test 3.8 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 852aef6..1c85441 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,7 @@ jobs: - python: '3.5' - python: '3.6' # 3.7 is below + - python: '3.8' # PyPy (in official virtualenv) - python: pypy3 From 8521b4c6ab531d52c01aa53b5e64c1ffda8455cc Mon Sep 17 00:00:00 2001 From: Chris Down Date: Fri, 3 Jan 2020 12:25:55 +0000 Subject: [PATCH 13/13] Bump version to 2.0.0 --- setup.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 6aa9922..f86283f 100755 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( name="tzupdate", - version="1.5.0", + version="2.0.0", description="Set the system timezone based on IP geolocation", long_description=README, url="https://github.com/cdown/tzupdate", @@ -22,8 +22,6 @@ "Development Status :: 5 - Production/Stable", "License :: Public Domain", "Operating System :: POSIX :: Linux", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6",