diff --git a/.travis.yml b/.travis.yml index 5a1c6045..e7e9e55e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,24 +1,14 @@ language: python -dist: trusty python: - "3.6" - "3.5" - - "3.4" - - "3.3" - - "3.2" - "2.7" - "pypy" - "pypy3" -matrix: - include: - - python: 3.7 - dist: xenial - sudo: true + install: "pip install -e .[test]" script: make test before_install: - # Coverage 4.0 no longer supports py3.2 and codecov depends on latest coverage - - if [[ $TRAVIS_PYTHON_VERSION == '3.2' ]]; then pip install "coverage<4.0dev"; fi - pip install codecov after_success: - codecov diff --git a/gpiozero/compat.py b/gpiozero/compat.py index 04e506f6..41486221 100644 --- a/gpiozero/compat.py +++ b/gpiozero/compat.py @@ -38,64 +38,14 @@ print_function, division, ) -str = type('') -import math -import cmath -import weakref import operator import functools -# Handles pre 3.3 versions of Python without collections.abc -try: - from collections.abc import Mapping -except ImportError: - from collections import Mapping - -# Back-ported from python 3.5; see -# github.com/PythonCHB/close_pep/blob/master/is_close.py for original -# implementation -def isclose(a, b, rel_tol=1e-9, abs_tol=0.0): - if rel_tol < 0.0 or abs_tol < 0.0: - raise ValueError('error tolerances must be non-negative') - if a == b: # fast-path for exact equality - return True - if cmath.isinf(a) or cmath.isinf(b): - return False - diff = abs(b - a) - return ( - (diff <= abs(rel_tol * b)) or - (diff <= abs(rel_tol * a)) or - (diff <= abs_tol) - ) - - -# Backported from py3.4 -def mean(data): - if iter(data) is data: - data = list(data) - n = len(data) - if not n: - raise ValueError('cannot calculate mean of empty data') - return sum(data) / n +from collections.abc import Mapping -# Backported from py3.4 -def median(data): - data = sorted(data) - n = len(data) - if not n: - raise ValueError('cannot calculate median of empty data') - elif n % 2: - return data[n // 2] - else: - i = n // 2 - return (data[i - 1] + data[i]) / 2 - - -# Backported from py3.3 -def log2(x): - return math.log(x, 2) +str = type('') # Copied from the MIT-licensed https://github.com/slezica/python-frozendict @@ -124,58 +74,3 @@ def __hash__(self): hashes = map(hash, self.items()) self.__hash = functools.reduce(operator.xor, hashes, 0) return self.__hash - - -# Backported from py3.4 -class WeakMethod(weakref.ref): - """ - A custom `weakref.ref` subclass which simulates a weak reference to - a bound method, working around the lifetime problem of bound methods. - """ - - __slots__ = "_func_ref", "_meth_type", "_alive", "__weakref__" - - def __new__(cls, meth, callback=None): - try: - obj = meth.__self__ - func = meth.__func__ - except AttributeError: - raise TypeError("argument should be a bound method, not {0}" - .format(type(meth))) - def _cb(arg): - # The self-weakref trick is needed to avoid creating a reference - # cycle. - self = self_wr() - if self._alive: - self._alive = False - if callback is not None: - callback(self) - self = weakref.ref.__new__(cls, obj, _cb) - self._func_ref = weakref.ref(func, _cb) - self._meth_type = type(meth) - self._alive = True - self_wr = weakref.ref(self) - return self - - def __call__(self): - obj = super(WeakMethod, self).__call__() - func = self._func_ref() - if obj is None or func is None: - return None - return self._meth_type(func, obj) - - def __eq__(self, other): - if isinstance(other, WeakMethod): - if not self._alive or not other._alive: - return self is other - return weakref.ref.__eq__(self, other) and self._func_ref == other._func_ref - return False - - def __ne__(self, other): - if isinstance(other, WeakMethod): - if not self._alive or not other._alive: - return self is not other - return weakref.ref.__ne__(self, other) or self._func_ref != other._func_ref - return True - - __hash__ = weakref.ref.__hash__ diff --git a/gpiozero/devices.py b/gpiozero/devices.py index 1939fe7e..927a951d 100644 --- a/gpiozero/devices.py +++ b/gpiozero/devices.py @@ -45,9 +45,6 @@ from collections import namedtuple, OrderedDict from itertools import chain from types import FunctionType -from threading import Lock - -from .pins import Pin from .threads import _threads_shutdown from .mixins import ( ValuesMixin, diff --git a/gpiozero/input_devices.py b/gpiozero/input_devices.py index 2db51d17..f916d564 100644 --- a/gpiozero/input_devices.py +++ b/gpiozero/input_devices.py @@ -44,10 +44,7 @@ import warnings from time import sleep, time from threading import Event, Lock -try: - from statistics import median -except ImportError: - from .compat import median +from statistics import median from .exc import InputDeviceError, DeviceClosed, DistanceSensorNoEcho, \ PinInvalidState, PWMSoftwareFallback diff --git a/gpiozero/mixins.py b/gpiozero/mixins.py index 55acb5c0..8af6f309 100644 --- a/gpiozero/mixins.py +++ b/gpiozero/mixins.py @@ -43,10 +43,7 @@ from functools import wraps, partial from threading import Event from collections import deque -try: - from statistics import median -except ImportError: - from .compat import median +from statistics import median import warnings from .threads import GPIOThread diff --git a/gpiozero/output_devices.py b/gpiozero/output_devices.py index df1b1933..fee405b3 100644 --- a/gpiozero/output_devices.py +++ b/gpiozero/output_devices.py @@ -46,10 +46,7 @@ from itertools import repeat, cycle, chain from colorzero import Color from collections import OrderedDict -try: - from math import log2 -except ImportError: - from .compat import log2 +from math import log2 import warnings from .exc import OutputDeviceBadValue, GPIOPinMissing, PWMSoftwareFallback diff --git a/gpiozero/pins/local.py b/gpiozero/pins/local.py index 1c40c470..29e9e086 100644 --- a/gpiozero/pins/local.py +++ b/gpiozero/pins/local.py @@ -41,7 +41,6 @@ import io import errno import struct -import warnings from collections import defaultdict from threading import Lock try: diff --git a/gpiozero/pins/mock.py b/gpiozero/pins/mock.py index 02a354f0..a0ae08ce 100644 --- a/gpiozero/pins/mock.py +++ b/gpiozero/pins/mock.py @@ -41,10 +41,7 @@ from collections import namedtuple from time import time, sleep from threading import Thread, Event -try: - from math import isclose -except ImportError: - from ..compat import isclose +from math import isclose import pkg_resources diff --git a/gpiozero/pins/native.py b/gpiozero/pins/native.py index f2fd4797..0f3e4e1a 100644 --- a/gpiozero/pins/native.py +++ b/gpiozero/pins/native.py @@ -43,10 +43,8 @@ import errno import struct import select -import warnings from time import sleep from threading import Thread, Event, RLock -from collections import Counter try: from queue import Queue, Empty except ImportError: diff --git a/gpiozero/pins/pi.py b/gpiozero/pins/pi.py index 6514ad58..8e8bb85a 100644 --- a/gpiozero/pins/pi.py +++ b/gpiozero/pins/pi.py @@ -35,15 +35,10 @@ ) str = type('') -import io -from threading import RLock, Lock -from types import MethodType -from collections import defaultdict -try: - from weakref import ref, WeakMethod -except ImportError: - from ..compat import WeakMethod +from threading import RLock +from types import MethodType +from weakref import ref, WeakMethod import warnings try: @@ -56,7 +51,6 @@ from ..exc import ( PinNoPins, PinNonPhysical, - PinInvalidPin, SPIBadArgs, SPISoftwareFallback, ) diff --git a/gpiozero/tones.py b/gpiozero/tones.py index 1fe5f8c7..330e0182 100644 --- a/gpiozero/tones.py +++ b/gpiozero/tones.py @@ -40,11 +40,7 @@ import re import warnings -from collections import namedtuple -try: - from math import log2 -except ImportError: - from .compat import log2 +from math import log2 from .exc import AmbiguousTone diff --git a/gpiozero/tools.py b/gpiozero/tools.py index d6b86e49..5504d4bf 100644 --- a/gpiozero/tools.py +++ b/gpiozero/tools.py @@ -48,15 +48,8 @@ except ImportError: pass from itertools import cycle -from math import sin, cos, pi -try: - from statistics import mean -except ImportError: - from .compat import mean -try: - from math import isclose -except ImportError: - from .compat import isclose +from math import sin, cos, pi, isclose +from statistics import mean def _normalize(values): diff --git a/setup.py b/setup.py index 7a14c23f..b6873248 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -"A simple interface to GPIO devices with Raspberry Pi." +"""A simple interface to GPIO devices with Raspberry Pi.""" import io import os @@ -10,8 +10,8 @@ if not sys.version_info >= (2, 7): raise ValueError('This package requires Python 2.7 or above') elif sys.version_info[0] == 3: - if not sys.version_info >= (3, 2): - raise ValueError('This package requires Python 3.2 or above') + if not sys.version_info >= (3, 5): + raise ValueError('This package requires Python 3.5 or above') else: raise ValueError('Unrecognized major version of Python') @@ -40,9 +40,6 @@ "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.2", - "Programming Language :: Python :: 3.3", - "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", @@ -63,19 +60,6 @@ 'test': ['pytest', 'coverage', 'mock'], } -if sys.version_info[:2] == (3, 2): - # Particular versions are required for Python 3.2 compatibility - __extra_requires__['doc'].extend([ - 'Jinja2<2.7', - 'MarkupSafe<0.16', - ]) - __extra_requires__['test'][0] = 'pytest<3.0dev' - __extra_requires__['test'][1] = 'coverage<4.0dev' -elif sys.version_info[:2] == (3, 3): - __extra_requires__['test'][0] = 'pytest<3.3dev' -elif sys.version_info[:2] == (3, 4): - __extra_requires__['test'][0] = 'pytest<5.0dev' - try: # If we're executing on a Raspberry Pi, install all GPIO libraries for # testing (except RPIO which doesn't work on the multi-core models yet) diff --git a/tests/test_compat.py b/tests/test_compat.py index 473f6c90..d9f2c42e 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -36,14 +36,9 @@ ) str = type('') - -import gc -import sys import pytest -import random -import weakref -from gpiozero.compat import * +from gpiozero.compat import frozendict def test_frozendict(): @@ -64,306 +59,3 @@ def test_frozendict(): h = hash(f) assert h is not None assert hash(f) == h - -# ported from the official test cases; see -# https://github.com/python/cpython/blob/master/Lib/test/test_math.py for original - -NAN = float('nan') -INF = float('inf') -NINF = float('-inf') - -def test_isclose_negative_tolerances(): - with pytest.raises(ValueError): - isclose(1, 1, rel_tol=-1e-100) - with pytest.raises(ValueError): - isclose(1, 1, rel_tol=1e-100, abs_tol=-1e10) - -def test_isclose_identical(): - examples = [ - (2.0, 2.0), - (0.1e200, 0.1e200), - (1.123e-300, 1.123e-300), - (12345, 12345.0), - (0.0, -0.0), - (345678, 345678), - ] - for a, b in examples: - assert isclose(a, b, rel_tol=0.0, abs_tol=0.0) - -def test_isclose_eight_decimals(): - examples = [ - (1e8, 1e8 + 1), - (-1e-8, -1.000000009e-8), - (1.12345678, 1.12345679), - ] - for a, b in examples: - assert isclose(a, b, rel_tol=1e-8) - assert not isclose(a, b, rel_tol=1e-9) - -def test_isclose_near_zero(): - examples = [1e-9, -1e-9, -1e-150] - for a in examples: - assert not isclose(a, 0.0, rel_tol=0.9) - assert isclose(a, 0.0, abs_tol=1e-8) - -def test_isclose_inf(): - assert isclose(INF, INF) - assert isclose(INF, INF, abs_tol=0.0) - assert isclose(NINF, NINF) - assert isclose(NINF, NINF, abs_tol=0.0) - -def test_isclose_inf_ninf_nan(): - examples = [ - (NAN, NAN), - (NAN, 1e-100), - (1e-100, NAN), - (INF, NAN), - (NAN, INF), - (INF, NINF), - (INF, 1.0), - (1.0, INF), - (INF, 1e308), - (1e308, INF), - ] - for a, b in examples: - assert not isclose(a, b, abs_tol=0.999999999999999) - -def test_isclose_zero_tolerance(): - examples = [ - (1.0, 1.0), - (-3.4, -3.4), - (-1e-300, -1e-300), - ] - for a, b in examples: - assert isclose(a, b, rel_tol=0.0) - examples = [ - (1.0, 1.000000000000001), - (0.99999999999999, 1.0), - (1.0e200, .999999999999999e200), - ] - for a, b in examples: - assert not isclose(a, b, rel_tol=0.0) - -def test_isclose_assymetry(): - assert isclose(9, 10, rel_tol=0.1) - assert isclose(10, 9, rel_tol=0.1) - -def test_isclose_integers(): - examples = [ - (100000001, 100000000), - (123456789, 123456788), - ] - for a, b in examples: - assert isclose(a, b, rel_tol=1e-8) - assert not isclose(a, b, rel_tol=1e-9) - -# ported from the official test cases; see -# https://github.com/python/cpython/blob/master/Lib/test/test_statistics.py for -# original - -def test_mean(): - examples = [ - (4.8125, (0, 1, 2, 3, 3, 3, 4, 5, 5, 6, 7, 7, 7, 7, 8, 9)), - (22.015625, (17.25, 19.75, 20.0, 21.5, 21.75, 23.25, 25.125, 27.5)), - (INF, (1, 3, 5, 7, 9, INF)), - (NINF, (1, 3, 5, 7, 9, NINF)), - ] - for result, values in examples: - values = list(values) - random.shuffle(values) - assert mean(values) == result - assert mean(iter(values)) == result - -def test_mean_big_data(): - c = 1e9 - data = [3.4, 4.5, 4.9, 6.7, 6.8, 7.2, 8.0, 8.1, 9.4] - expected = mean(data) + c - assert expected != c - assert mean([x + c for x in data]) == expected - -def test_mean_doubled_data(): - data = [random.uniform(-3, 5) for _ in range(1000)] - expected = mean(data) - actual = mean(data * 2) - assert isclose(expected, actual) - -def test_mean_empty(): - with pytest.raises(ValueError): - mean(()) - -def test_median(): - assert median([1, 2, 3, 4, 5, 6]) == 3.5 - assert median([1, 2, 3, 4, 5, 6, 9]) == 4 - -def test_median_empty(): - with pytest.raises(ValueError): - median(()) - -# ported from the official test cases; see -# https://github.com/python/cpython/blob/master/Lib/test/test_weakref.py for -# original - -class Object(object): - def __init__(self, arg): - self.arg = arg - def __repr__(self): - return "" % self.arg - def __eq__(self, other): - if isinstance(other, Object): - return self.arg == other.arg - return NotImplemented - def __ne__(self, other): - if isinstance(other, Object): - return self.arg != other.arg - return NotImplemented - def __lt__(self, other): - if isinstance(other, Object): - return self.arg < other.arg - return NotImplemented - def __hash__(self): - return hash(self.arg) - def some_method(self): - return 4 - def other_method(self): - return 5 - -@pytest.fixture() -def subclass(request): - class C(Object): - def some_method(self): - return 6 - return C - - -def test_weakmethod_alive(): - o = Object(1) - r = WeakMethod(o.some_method) - assert isinstance(r, weakref.ReferenceType) - assert isinstance(r(), type(o.some_method)) - assert r().__self__ is o - assert r().__func__ is o.some_method.__func__ - assert r()() == 4 - -def test_weakmethod_object_dead(): - o = Object(1) - r = WeakMethod(o.some_method) - del o - gc.collect() - assert r() is None - -@pytest.mark.xfail((3, 2) <= sys.version_info[:2] <= (3, 3), - reason='intermittent failure on py3.2 and py3.3') -def test_weakmethod_method_dead(subclass): - o = subclass(1) - r = WeakMethod(o.some_method) - del subclass.some_method - gc.collect() - assert r() is None - -def test_weakmethod_callback_when_object_dead(subclass): - calls = [] - def cb(arg): - calls.append(arg) - o = subclass(1) - r = WeakMethod(o.some_method, cb) - del o - gc.collect() - assert calls == [r] - # Callback is only called once - subclass.some_method = Object.some_method - gc.collect() - assert calls == [r] - -@pytest.mark.xfail((3, 2) <= sys.version_info[:2] <= (3, 3), - reason='intermittent failure on py3.2 and py3.3') -def test_weakmethod_callback_when_method_dead(subclass): - calls = [] - def cb(arg): - calls.append(arg) - o = subclass(1) - r = WeakMethod(o.some_method, cb) - del subclass.some_method - gc.collect() - assert calls == [r] - # Callback is only called once - del o - gc.collect() - assert calls == [r] - -@pytest.mark.xfail(hasattr(sys, 'pypy_version_info'), - reason='pypy memory management is different') -def test_weakmethod_no_cycles(): - o = Object(1) - def cb(_): - pass - r = WeakMethod(o.some_method, cb) - wr = weakref.ref(r) - del r - assert wr() is None - -def test_weakmethod_equality(): - def _eq(a, b): - assert a == b - assert not (a != b) - def _ne(a, b): - assert not (a == b) - assert a != b - x = Object(1) - y = Object(1) - a = WeakMethod(x.some_method) - b = WeakMethod(y.some_method) - c = WeakMethod(x.other_method) - d = WeakMethod(y.other_method) - # Objects equal, same method - _eq(a, b) - _eq(c, d) - # Objects equal, different method - _ne(a, c) - _ne(a, d) - _ne(b, c) - _ne(b, d) - # Objects unequal, same or different method - z = Object(2) - e = WeakMethod(z.some_method) - f = WeakMethod(z.other_method) - _ne(a, e) - _ne(a, f) - _ne(b, e) - _ne(b, f) - del x, y, z - gc.collect() - # Dead WeakMethods compare by identity - refs = a, b, c, d, e, f - for q in refs: - for r in refs: - assert (q == r) == (q is r) - assert (q != r) == (q is not r) - -def test_weakmethod_hashing(): - x = Object(1) - y = Object(1) - a = WeakMethod(x.some_method) - b = WeakMethod(y.some_method) - c = WeakMethod(y.other_method) - # Since WeakMethod objects are equal, the hashes should be equal - assert hash(a) == hash(b) - ha = hash(a) - # Dead WeakMethods retain their old hash value - del x, y - gc.collect() - assert hash(a) == ha - assert hash(b) == ha - # If it wasn't hashed when alive, a dead WeakMethod cannot be hashed - with pytest.raises(TypeError): - hash(c) - -def test_weakmethod_bad_method(): - with pytest.raises(TypeError): - WeakMethod('foo') - -def test_weakmethod_other_equality(): - x = Object(1) - a = WeakMethod(x.some_method) - b = WeakMethod(x.other_method) - assert not a == 1 - assert a != 1 diff --git a/tests/test_outputs.py b/tests/test_outputs.py index 9a70992c..8e0c145e 100644 --- a/tests/test_outputs.py +++ b/tests/test_outputs.py @@ -40,10 +40,7 @@ import sys from time import sleep, time -try: - from math import isclose -except ImportError: - from gpiozero.compat import isclose +from math import isclose import pytest from colorzero import Color, Red, Green, Blue diff --git a/tests/test_real_pins.py b/tests/test_real_pins.py index ec93d007..6795b612 100644 --- a/tests/test_real_pins.py +++ b/tests/test_real_pins.py @@ -43,6 +43,7 @@ import os import errno from time import time, sleep +from math import isclose import pytest import pkg_resources @@ -50,10 +51,6 @@ from gpiozero import * from gpiozero.pins.mock import MockConnectedPin, MockFactory from gpiozero.pins.native import NativeFactory -try: - from math import isclose -except ImportError: - from gpiozero.compat import isclose # This module assumes you've wired the following GPIO pins together. The pins diff --git a/tests/test_spi.py b/tests/test_spi.py index 0a96936a..9d1cba03 100644 --- a/tests/test_spi.py +++ b/tests/test_spi.py @@ -39,11 +39,9 @@ import io -import sys import pytest from array import array from mock import patch -from collections import namedtuple from gpiozero.pins.native import NativeFactory from gpiozero.pins.local import ( diff --git a/tests/test_spi_devices.py b/tests/test_spi_devices.py index 2af0c1d5..dc142335 100644 --- a/tests/test_spi_devices.py +++ b/tests/test_spi_devices.py @@ -42,10 +42,7 @@ import pytest from mock import patch from collections import namedtuple -try: - from math import isclose -except ImportError: - from gpiozero.compat import isclose +from math import isclose from gpiozero.pins.mock import MockSPIDevice, MockPin from gpiozero import * diff --git a/tests/test_tools.py b/tests/test_tools.py index 26c4b668..6a8e14f8 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -39,24 +39,13 @@ import pytest -from math import sin, cos, radians +from math import sin, cos, radians, isclose +from statistics import mean, median from time import time, sleep from itertools import islice from gpiozero import Device, LED, Button, Robot from gpiozero.tools import * -try: - from math import isclose -except ImportError: - from gpiozero.compat import isclose -try: - from statistics import mean -except ImportError: - from gpiozero.compat import mean -try: - from statistics import median -except ImportError: - from gpiozero.compat import median epsilon = 0.01 # time to sleep after setting source before checking value diff --git a/tox.ini b/tox.ini index c90b45b2..9a314f19 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = {py27,py32,py33,py34,py35,py36,py37} +envlist = {py27,py35,py36,py37} [testenv] deps = .[test] @@ -10,58 +10,3 @@ setenv = COVERAGE_FILE=.coverage.{envname} GPIOZERO_TEST_LOCK={toxworkdir}/real_pins_lock passenv = GPIOZERO_* COVERAGE_* - -[testenv:py32] -# All this shenanigans is to get tox (and everything else) working with python -# 3.2. We stop venv for downloading or installing anything in the venv, because -# all the local stuff almost certainly doesn't work on py3.2. -basepython = python3.2 -setenv = - VIRTUALENV_NO_DOWNLOAD=1 - VIRTUALENV_NO_PIP=1 - VIRTUALENV_NO_WHEEL=1 - VIRTUALENV_NO_SETUPTOOLS=1 - COVERAGE_FILE=.coverage.{envname} - GPIOZERO_TEST_LOCK={toxworkdir}/real_pins_lock -# The following lines are needed to stop tox trying to install dependencies (or -# anything else) itself because pip won't exist in the venv yet. -whitelist_externals = - echo - curl - pip - make -deps = -list_dependencies_command = echo -skip_install = true -# Now do everything manually... -commands = - curl https://bootstrap.pypa.io/3.2/get-pip.py -o {envdir}/get-pip32.py - python {envdir}/get-pip32.py - pip install -e .[test] - make test -passenv = GPIOZERO_* COVERAGE_* - -[testenv:py33] -# Same story as above -basepython = python3.3 -setenv = - VIRTUALENV_NO_DOWNLOAD=1 - VIRTUALENV_NO_PIP=1 - VIRTUALENV_NO_WHEEL=1 - VIRTUALENV_NO_SETUPTOOLS=1 - COVERAGE_FILE=.coverage.{envname} - GPIOZERO_TEST_LOCK={toxworkdir}/real_pins_lock -whitelist_externals = - echo - curl - pip - make -deps = -list_dependencies_command = echo -skip_install = true -commands = - curl https://bootstrap.pypa.io/3.3/get-pip.py -o {envdir}/get-pip33.py - python {envdir}/get-pip33.py - pip install -e .[test] - make test -passenv = GPIOZERO_* COVERAGE_*