diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..c1f72df --- /dev/null +++ b/.coveragerc @@ -0,0 +1,4 @@ +[run] +source = gpio +omit = + .tox/* \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..210a2a3 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,32 @@ +name: Python Tests + +on: + pull_request: + push: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python: [2.7, 3.5, 3.7, 3.9] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + - name: Install Dependencies + run: | + python -m pip install --upgrade setuptools tox + - name: Run Tests + run: | + tox -e py + - name: Coverage + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + python -m pip install coveralls + coveralls --service=github + if: ${{ matrix.python == '3.9' }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 61153fb..2d40554 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ __pycache__ *.pyc *.egg* +.coverage +.tox/ +build/ +dist/ diff --git a/MANIFEST.in b/MANIFEST.in index bb3ec5f..cec267d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,4 @@ include README.md +include LICENSE.txt +include setup.py +recursive-include gpio *.py \ No newline at end of file diff --git a/README.md b/README.md index f16c30e..4df6716 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,49 @@ It is intended to mimick [RPIO](http://pythonhosted.org/RPIO/) as much as possib for all features, while also supporting additional (and better named) functionality to the same methods. - ## Supported Features + - get pin values with `read(pin)` or `input(pin)` -- set pin values with `set(pin, value)` or `output(pin, value)` +- set pin values with `write(pin, value)`, `set(pin, value)` or `output(pin, value)` - get the pin mode with `mode(pin)` - set the pin mode with `setup(pin, mode)` - `mode` can currently equal `gpio.IN` or `gpio.OUT` +- create a `GPIOPin` class directly to `write` and `read` a pin + +## Examples + +### RPi.GPIO Drop-in + +Good for up to 130KHz pin toggle on a Pi 400. + +```python +import time + +import gpio as GPIO + +GPIO.setup(14, GPIO.OUT) + +while True: + GPIO.output(14, GPIO.HIGH) + time.sleep(1.0) + GPIO.output(14, GPIO.LOW) + time.sleep(1.0) +``` + +### Use GPIOPin directly + +Good for up to 160KHz pin toggle on a Pi 400. + +This gives you a class instance you can manipulate directly, eliminating the lookup: + +```python +import gpio + +pin = gpio.GPIOPin(14, gpio.OUT) + +while True: + pin.write(14, GPIO.HIGH) + time.sleep(1.0) + pin.write(14, GPIO.LOW) + time.sleep(1.0) +``` diff --git a/gpio.py b/gpio.py deleted file mode 100644 index 1c893ba..0000000 --- a/gpio.py +++ /dev/null @@ -1,208 +0,0 @@ -# -*- coding: utf-8 -*- -__version__ = '0.3.0' - -import threading -import os - -import logging -log = logging.getLogger(__name__) - - -class PinState(object): - """An ultra simple pin-state object. - - Keeps track data related to each pin. - - Args: - value: the file pointer to set/read value of pin. - direction: the file pointer to set/read direction of the pin. - active_now: the file pointer to set/read if the pin is active_low. - """ - def __init__(self, value, direction, active_low): - self.value = value - self.direction = direction - self.active_low = active_low - -path = os.path -pjoin = os.path.join - -gpio_root = '/sys/class/gpio' -gpiopath = lambda pin: os.path.join(gpio_root, 'gpio{0}'.format(pin)) -_export_lock = threading.Lock() - -_pyset = set - -_open = dict() -FMODE = 'w+' - -IN, OUT = 'in', 'out' -LOW, HIGH = 'low', 'high' - - -def _write(f, v): - log.debug("writing: {0}: {1}".format(f, v)) - f.write(str(v)) - f.flush() - - -def _read(f): - log.debug("Reading: {0}".format(f)) - f.seek(0) - return f.read().strip() - - -def _verify(function): - """decorator to ensure pin is properly set up""" - # @functools.wraps - def wrapped(pin, *args, **kwargs): - pin = int(pin) - if pin not in _open: - ppath = gpiopath(pin) - if not os.path.exists(ppath): - log.debug("Creating Pin {0}".format(pin)) - with _export_lock: - with open(pjoin(gpio_root, 'export'), 'w') as f: - _write(f, pin) - value, direction, active_low = None, None, None - try: - value = open(pjoin(ppath, 'value'), FMODE) - direction = open(pjoin(ppath, 'direction'), FMODE) - active_low = open(pjoin(ppath, 'active_low'), FMODE) - except Exception as e: - if value: value.close() - if direction: direction.close() - if active_low: active_low.close() - raise e - _open[pin] = PinState(value=value, direction=direction, active_low=active_low) - return function(pin, *args, **kwargs) - return wrapped - - -def cleanup(pin=None, assert_exists=False): - """Cleanup the pin by closing and unexporting it. - - Args: - pin (int, optional): either the pin to clean up or None (default). - If None, clean up all pins. - assert_exists: if True, raise a ValueError if the pin was not - setup. Otherwise, this function is a NOOP. - """ - if pin is None: - # Take a list of keys because we will be deleting from _open - for pin in list(_open): - cleanup(pin) - return - if not isinstance(pin, int): - raise TypeError("pin must be an int, got: {}".format(pin)) - - state = _open.get(pin) - if state is None: - if assert_exists: - raise ValueError("pin {} was not setup".format(pin)) - return - state.value.close() - state.direction.close() - state.active_low.close() - if os.path.exists(gpiopath(pin)): - log.debug("Unexporting pin {0}".format(pin)) - with _export_lock: - with open(pjoin(gpio_root, 'unexport'), 'w') as f: - _write(f, pin) - - del _open[pin] - - -@_verify -def setup(pin, mode, pullup=None, initial=False, active_low=None): - '''Setup pin with mode IN or OUT. - - Args: - pin (int): - mode (str): use either gpio.OUT or gpio.IN - pullup (None): rpio compatibility. If anything but None, raises - value Error - initial (bool, optional): Initial pin value. Default is False - active_low (bool, optional): Set the pin to active low. Default - is None which leaves things as configured in sysfs - ''' - if pullup is not None: - raise ValueError("sysfs does not support pullups") - - if mode not in (IN, OUT, LOW, HIGH): - raise ValueError(mode) - - if active_low is not None: - if not isinstance(active_low, bool): - raise ValueError("active_low argument must be True or False") - log.debug("Set active_low {0}: {1}".format(pin, active_low)) - f_active_low = _open[pin].active_low - _write(f_active_low, int(active_low)) - - log.debug("Setup {0}: {1}".format(pin, mode)) - f_direction = _open[pin].direction - _write(f_direction, mode) - if mode == OUT: - if initial: - set(pin, 1) - else: - set(pin, 0) - - -@_verify -def mode(pin): - '''get the pin mode - - Returns: - str: "in" or "out" - ''' - f = _open[pin].direction - return _read(f) - - -@_verify -def read(pin): - '''read the pin value - - Returns: - bool: 0 or 1 - ''' - f = _open[pin].value - out = int(_read(f)) - log.debug("Read {0}: {1}".format(pin, out)) - return out - - -@_verify -def set(pin, value): - '''set the pin value to 0 or 1''' - if value is LOW: - value = 0 - value = int(bool(value)) - log.debug("Write {0}: {1}".format(pin, value)) - f = _open[pin].value - _write(f, value) - - -@_verify -def input(pin): - '''read the pin. Same as read''' - return read(pin) - - -@_verify -def output(pin, value): - '''set the pin. Same as set''' - return set(pin, value) - - -def setwarnings(value): - '''exists for rpio compatibility''' - pass - - -def setmode(value): - '''exists for rpio compatibility''' - pass - - -BCM = None # rpio compatibility diff --git a/gpio/__init__.py b/gpio/__init__.py new file mode 100644 index 0000000..c6f37f7 --- /dev/null +++ b/gpio/__init__.py @@ -0,0 +1,269 @@ +# -*- coding: utf-8 -*- +__version__ = '1.0.0' + +from threading import Lock +try: + from collections.abc import Iterable +except ImportError: + from collections import Iterable +import os + + +_export_lock = Lock() +_open_pins = {} + + +GPIO_ROOT = '/sys/class/gpio' +GPIO_EXPORT = os.path.join(GPIO_ROOT, 'export') +GPIO_UNEXPORT = os.path.join(GPIO_ROOT, 'unexport') +FMODE = 'w+' # w+ overwrites and truncates existing files +IN, OUT = 'in', 'out' +LOW, HIGH = 0, 1 + + +class GPIOPin(object): + """Handle pin state. + + Create a singleton instance of a GPIOPin(n) and track its state internally. + + Args: + pin (int): the pin to configure + mode (str): use either gpio.OUT or gpio.IN + initial (bool, optional): Initial pin value. Default is LOW + active_low (bool, optional): Set the pin to active low. Default + is None which leaves things as configured in sysfs + Raises: + RuntimeError: if pin is already configured + """ + def __init__(self, pin, direction=None, initial=LOW, active_low=None): + # .configured() will raise a TypeError if "pin" is not convertable to int + if GPIOPin.configured(pin, False) is not None: + raise RuntimeError("pin {} is already configured".format(pin)) + + self.value = None + self.pin = int(pin) + self.root = os.path.join(GPIO_ROOT, 'gpio{0}'.format(self.pin)) + + if not os.path.exists(self.root): + with _export_lock: + with open(GPIO_EXPORT, FMODE) as f: + f.write(str(self.pin)) + f.flush() + + # Using unbuffered binary IO is ~ 3x faster than text + self.value = open(os.path.join(self.root, 'value'), 'wb+', buffering=0) + + # I hate manually calling .setup()! + self.setup(direction, initial, active_low) + + # Add class to open pins + _open_pins[self.pin] = self + + def setup(self, direction=None, initial=LOW, active_low=None): + if direction is not None: + self.set_direction(direction) + + if active_low is not None: + self.set_active_low(active_low) + + if direction == OUT: + self.write(initial) + + @staticmethod + def configured(pin, assert_configured=True): + """Get a configured GPIOPin instance where available. + + Args: + pin (int): the pin to check + assert_configured (bool): True to raise exception if pin unconfigured + + Returns: + object: GPIOPin if configured, otherwise None + + Raises: + RuntimeError: if pin is not configured + """ + try: + # Implicitly convert str to int, ie: "1" -> 1 + pin = int(pin) + except (TypeError, ValueError): + raise ValueError("pin must be an int") + + if pin not in _open_pins and assert_configured: + raise RuntimeError("pin {} is not configured".format(pin)) + + return _open_pins.get(pin) + + def get_direction(self): + '''Get the direction of pin + + Returns: + str: "in" or "out" + ''' + with open(os.path.join(self.root, 'direction'), FMODE) as f: + return f.read().strip() + + def set_direction(self, mode): + '''Set the direction of pin + + Args: + mode (str): use either gpio.OUT or gpio.IN + ''' + if mode not in (IN, OUT, LOW, HIGH): + raise ValueError("Unsupported pin mode {}".format(mode)) + + with open(os.path.join(self.root, 'direction'), FMODE) as f: + f.write(str(mode)) + f.flush() + + def set_active_low(self, active_low): + '''Set the polarity of pin + + Args: + mode (bool): True = active low / False = active high + ''' + if not isinstance(active_low, bool): + raise ValueError("active_low must be True or False") + + with open(os.path.join(self.root, 'active_low'), FMODE) as f: + f.write('1' if active_low else '0') + f.flush() + + def read(self): + '''Read pin value + + Returns: + int: gpio.HIGH or gpio.LOW + ''' + self.value.seek(0) + value = self.value.read() + try: + # Python > 3 - bytes + # Subtracting 48 converts an ASCII "0" or "1" to an int + # ord("0") == 48 + return value[0] - 48 + except TypeError: + # Python 2.x - str + return int(value) + + def write(self, value): + '''Write pin value + + Args: + value (bool): use either gpio.HIGH or gpio.LOW + ''' + # write as bytes, about 3x faster than string IO + self.value.write(b'1' if value else b'0') + + def cleanup(self): + '''Clean up pin + + Unexports the pin and deletes it from the open list. + + ''' + # Note: I have not put "cleanup" into the __del__ method since it's not + # always desireable to unexport pins at program exit. + # Additionally "open" can be deleted *before* the GPIOPin instance. + self.value.close() + + if os.path.exists(self.root): + with _export_lock: + with open(GPIO_UNEXPORT, FMODE) as f: + f.write(str(self.pin)) + f.flush() + + del _open_pins[self.pin] + + +def cleanup(pin=None, assert_exists=False): + """Cleanup the pin by closing and unexporting it. + + Args: + pin (int, optional): either the pin to clean up or None (default). + If None, clean up all pins. + assert_exists: if True, raise a ValueError if the pin was not + setup. Otherwise, this function is a NOOP. + """ + # Note: since "pin" is a kwarg in this function, it has not been renamed it to "pins" above + pins = pin + + if pins is None: + # Must be converted to a list since _open_pins is potentially modified below + pins = list(_open_pins.keys()) + + if not isinstance(pins, Iterable): + pins = [pins] + + for pin in pins: + state = GPIOPin.configured(pin, assert_exists) + + if state is not None: + state.cleanup() # GPIOPin will remove itself from _open_pins + + +# TODO RPi.GPIO uses "pull_up_down", does rpio differ? +def setup(pins, mode, pullup=None, initial=LOW, active_low=None): + '''Setup pin with mode IN or OUT. + + Args: + pin (int): + mode (str): use either gpio.OUT or gpio.IN + pullup (None): rpio compatibility. If anything but None, raises + value Error + initial (bool, optional): Initial pin value. Default is LOW + active_low (bool, optional): Set the pin to active low. Default + is None which leaves things as configured in sysfs + ''' + if not isinstance(pins, Iterable): + pins = [pins] + + if pullup is not None: + raise ValueError("sysfs does not support pull up/down") + + for pin in pins: + state = GPIOPin.configured(pin, False) + + # Attempt to create the pin if not configured + if state is None: + state = GPIOPin(pin) # GPIOPin will add itself to _open_pins + + state.setup(mode, initial, active_low) + + +def mode(pin): + '''get the pin mode + + Returns: + str: "in" or "out" + ''' + return GPIOPin.configured(pin).get_direction() + + +def read(pin): + '''read the pin value + + Returns: + bool: either gpio.LOW or gpio.HIGH + ''' + # These function calls lose us a little speed + # but we're already > 2x faster so... + # If you want things to be faster use a GPIOPin instance directly. + return GPIOPin.configured(pin).read() + + +def write(pin, value): + '''set the pin value to LOW or HIGH + + Args: + pin (int): any configured pin + value (bool): use gpio.LOW or gpio.HIGH + ''' + # These function calls lose us a little speed + # but we're already > 2x faster so... + # If you want things to be faster use a GPIOPin instance directly. + GPIOPin.configured(pin).write(value) + + +input = read +output = write +set = write # TODO Set should be dropped, since it's a Python reserved word diff --git a/setup.cfg b/setup.cfg index b88034e..39054ab 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,38 @@ +# -*- coding: utf-8 -*- [metadata] -description-file = README.md +name = gpio +version = 1.0.0 +author = Garrett Berg, Phil Howard +author_email = phil@pimoroni.com +description = gpio access via the standard linux sysfs interface +long_description = file: README.md +long_description_content_type = text/markdown +keywords = gpio sysfs linux +license = MIT +license_files = LICENSE.txt +project_urls = + GitHub = https://github.com/vitiral/gpio +classifiers = + Intended Audience :: Developers + License :: OSI Approved :: MIT License + Operating System :: POSIX + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 3 + Topic :: Software Development :: Embedded Systems + Topic :: Software Development :: Libraries :: Python Modules + +[options] +python_requires = >= 2.7 +packages = gpio +install_requires = + +[flake8] +exclude = + .tox, + .eggs, + .git, + __pycache__, + build, + dist +ignore = + E501 \ No newline at end of file diff --git a/setup.py b/setup.py index 26a11db..53d3ffc 100644 --- a/setup.py +++ b/setup.py @@ -1,38 +1,4 @@ -try: - from setuptools import setup -except ImportError: - from distutils.core import setup +# -*- coding: utf-8 -*- +from setuptools import setup -from gpio import __version__ - - -with open("README.md") as f: - ldesc = f.read() - -config = { - 'name': 'gpio', - 'author': 'Garrett Berg', - 'author_email': 'garrett@cloudformdesign.com', - 'version': __version__, - 'py_modules': ['gpio'], - 'license': 'MIT', - 'install_requires': [ - ], - 'extras_require': { - }, - 'description': "gpio access via the standard linux sysfs interface", - 'long_description': ldesc, - 'url': "https://github.com/cloudformdesign/gpio", - 'classifiers': [ - # 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Operating System :: POSIX', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Topic :: Software Development :: Embedded Systems', - 'Topic :: Software Development :: Libraries :: Python Modules', - ] -} - -setup(**config) +setup() diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index 40a96af..0000000 --- a/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/tests/__init__.pyc b/tests/__init__.pyc deleted file mode 100644 index 47e2037..0000000 Binary files a/tests/__init__.pyc and /dev/null differ diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..6ceed2b --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,19 @@ +import sys + +import mock +import pytest + + +@pytest.fixture +def patch_open(): + mopen = mock.MagicMock() + bopen = "__builtin__.open" if sys.version_info.major < 3 else "builtins.open" + with mock.patch(bopen, mopen, create=True): + yield mopen + + +@pytest.fixture +def gpio(): + import gpio + yield gpio + del sys.modules['gpio'] \ No newline at end of file diff --git a/tests/test_gpio.py b/tests/test_gpio.py index af6f6a9..5120885 100644 --- a/tests/test_gpio.py +++ b/tests/test_gpio.py @@ -1,72 +1,235 @@ -# -*- coding: utf-8 -*- - -"""Unit tests. - -These don't seem to work on anymore (at least not on x86) -""" - - -from unittest import TestCase -try: - from unittest.mock import mock_open, patch -except ImportError: - from mock import mock_open, patch -import sys -import os -pjoin = os.path.join -import gpio - - -if sys.version_info.major < 3: - bins = '__builtin__' -else: - bins = 'builtins' - -root = gpio.gpio_root - - -def mockargs(mock): - return [m[0] for m in mock.call_args_list] - - -def assertInitialized(self, mfile, gpio=0): - margs = mockargs(mfile) - groot = pjoin(root, 'gpio{}'.format(gpio)) - self.assertEqual(margs[0], (pjoin(root, 'export'), 'w')) - self.assertEqual(margs[1], (pjoin(groot, 'value'), 'w+')) - self.assertEqual(margs[2], (pjoin(groot, 'direction'), 'w+')) - self.assertEqual(margs[3], (pjoin(groot, 'drive'), 'w+')) - - -def reset(method): - def wrapped(*args, **kwargs): - gpio._open.clear() - return method(*args, **kwargs) - return wrapped - - -class TestRead(TestCase): - @reset - def test_basic(self): - mopen = mock_open(read_data='0') - with patch(bins + '.open', mopen, create=True) as m: - result = gpio.read(0) - assertInitialized(self, m) - self.assertEqual(result, 0) - - -class TestWrite(TestCase): - @reset - def test_basic(self): - # with mock_open you have to remember that all files are the same - # mock object. - mopen = mock_open(read_data='0') - with patch(bins + '.open', mopen, create=True) as m: - gpio.setup(0, gpio.OUT) - gpio.set(0, 0) - assertInitialized(self, m) - # So, "value" could be "direction" or any other file - written = mockargs(gpio._open[0]['value'].write) - expected = [('0',), ('out',), ('0',)] - assertInitialized(self, m) - self.assertListEqual(written, expected) +import mock +import pytest + + +def test_setup_rpio(gpio, patch_open): + gpio.setup(10, gpio.OUT) + + patch_open.assert_any_call('/sys/class/gpio/export', 'w+') + patch_open().__enter__().write.assert_any_call('10') + + patch_open.assert_any_call('/sys/class/gpio/gpio10/value', 'wb+', buffering=0) + patch_open.assert_any_call('/sys/class/gpio/gpio10/direction', 'w+') + patch_open().__enter__().write.assert_any_call(str(gpio.OUT)) + + +def test_setup_class(gpio, patch_open): + pin = gpio.GPIOPin(10, gpio.OUT) + + patch_open.assert_any_call('/sys/class/gpio/export', 'w+') + patch_open().__enter__().write.assert_any_call('10') + + patch_open.assert_any_call('/sys/class/gpio/gpio10/value', 'wb+', buffering=0) + patch_open.assert_any_call('/sys/class/gpio/gpio10/direction', 'w+') + patch_open().__enter__().write.assert_any_call(str(gpio.OUT)) + + +def test_setup_rpio_list(gpio, patch_open): + gpio.setup([9, 10, 11], gpio.OUT) + + +def test_setup_rpio_tuple(gpio, patch_open): + gpio.setup((9, 10, 11), gpio.OUT) + + +def test_setup_rpio_generator(gpio, patch_open): + pins = {9: 9, 10: 10, 11: 11} + gpio.setup(pins.keys(), gpio.OUT) + + +def test_setup_with_pull(gpio, patch_open): + with pytest.raises(ValueError): + gpio.setup(10, gpio.OUT, pullup=1) + + +def test_class_already_setup(gpio, patch_open): + pin = gpio.GPIOPin(10, gpio.OUT) + + with pytest.raises(RuntimeError): + gpio.GPIOPin(10, gpio.OUT) + + +def test_rpio_already_setup(gpio, patch_open): + gpio.setup(10, gpio.OUT) + # Running gpio.setup again should not raise an error + # in RPi.GPIO this may raise a warning + gpio.setup(10, gpio.OUT) + + with pytest.raises(RuntimeError): + gpio.GPIOPin(10, gpio.OUT) + + +def test_rpio_cleanup_all(gpio, patch_open): + gpio.setup(10, gpio.OUT) + gpio.setup(11, gpio.OUT) + gpio.cleanup() + assert gpio.GPIOPin.configured(10, False) is None + assert gpio.GPIOPin.configured(11, False) is None + + +def test_rpio_cleanup_list(gpio, patch_open): + gpio.setup(10, gpio.OUT) + gpio.setup(11, gpio.OUT) + gpio.setup(12, gpio.OUT) + gpio.cleanup([10, 11]) + assert gpio.GPIOPin.configured(10, False) is None + assert gpio.GPIOPin.configured(11, False) is None + assert gpio.GPIOPin.configured(12, False) is not None + + +def test_rpio_invalid_cleanup(gpio, patch_open): + with pytest.raises(RuntimeError): + gpio.cleanup(10, True) + + +def test_rpio_invalid_cleanup_list(gpio, patch_open): + gpio.setup(10, gpio.OUT) + with pytest.raises(RuntimeError): + gpio.cleanup([10, 11], True) + + +def test_setup_class_registers_self(gpio, patch_open): + pin = gpio.GPIOPin(10, gpio.OUT) + assert gpio.GPIOPin.configured(10) == pin + + +def test_cleanup_class_unexports_pin(gpio, patch_open): + pin = gpio.GPIOPin(10, gpio.OUT) + patch_open.reset_mock() + pin.root = "/dev/null" # Pass os.path.exists check + pin.cleanup() + + patch_open.assert_any_call('/sys/class/gpio/unexport', 'w+') + patch_open().__enter__().write.assert_any_call('10') + + +def test_setup_pin_is_not_int(gpio, patch_open): + with pytest.raises(ValueError): + gpio.setup(None, gpio.OUT) + + with pytest.raises(ValueError): + pin = gpio.GPIOPin(None, gpio.OUT) + + +def test_cleanup_class_unregisters_self(gpio, patch_open): + pin = gpio.GPIOPin(10, gpio.OUT) + patch_open.reset_mock() + pin.cleanup() + assert gpio.GPIOPin.configured(10, False) == None + + +def test_set_direction(gpio, patch_open): + pin = gpio.GPIOPin(10, gpio.OUT) + patch_open.reset_mock() + pin.set_direction(gpio.OUT) + pin.set_direction(gpio.IN) + patch_open().__enter__().write.assert_any_call(str(gpio.OUT)) + patch_open().__enter__().write.assert_any_call(str(gpio.IN)) + + +def test_set_active_low(gpio, patch_open): + pin = gpio.GPIOPin(10, gpio.OUT) + + patch_open.reset_mock() + pin.set_active_low(False) + patch_open.assert_has_calls(( + mock.call().__enter__().write('0'), + mock.call().__enter__().flush(), + )) + + patch_open.reset_mock() + pin.set_active_low(True) + patch_open.assert_has_calls(( + mock.call().__enter__().write('1'), + mock.call().__enter__().flush(), + )) + + with pytest.raises(ValueError): + pin.set_active_low(None) + + +def test_setup_active_low(gpio, patch_open): + pin = gpio.GPIOPin(10, gpio.OUT, active_low=False) + patch_open.assert_has_calls(( + mock.call().__enter__().write('0'), + mock.call().__enter__().flush(), + )) + pin.cleanup() + + patch_open.reset_mock() + pin = gpio.GPIOPin(10, gpio.OUT, active_low=True) + patch_open.assert_has_calls(( + mock.call().__enter__().write('1'), + mock.call().__enter__().flush(), + )) + + +def test_get_direction(gpio, patch_open): + pin = gpio.GPIOPin(10, gpio.IN) + + patch_open().__enter__().read.return_value = 'in\n' + assert pin.get_direction() == gpio.IN + assert gpio.mode(10) == gpio.IN + + patch_open().__enter__().read.return_value = 'out\n' + assert pin.get_direction() == gpio.OUT + assert gpio.mode(10) == gpio.OUT + + +def test_set_direction(gpio, patch_open): + pin = gpio.GPIOPin(10, gpio.IN) + + for direction in (gpio.IN, gpio.OUT): + patch_open.reset_mock() + pin.set_direction(direction) + patch_open.assert_has_calls(( + mock.call().__enter__().write(direction), + )) + + with pytest.raises(ValueError): + pin.set_direction(None) + + +def test_unconfigured_runtimeerror(gpio, patch_open): + with pytest.raises(RuntimeError): + pin = gpio.GPIOPin.configured(10) + + +def test_write(gpio, patch_open): + pin = gpio.GPIOPin(10, gpio.OUT) + + patch_open.reset_mock() + pin.write(False) + patch_open.assert_has_calls(( + mock.call().write(b'0'), + )) + + patch_open.reset_mock() + gpio.write(10, False) + patch_open.assert_has_calls(( + mock.call().write(b'0'), + )) + + patch_open.reset_mock() + pin.write(True) + patch_open.assert_has_calls(( + mock.call().write(b'1'), + )) + + patch_open.reset_mock() + gpio.write(10, True) + patch_open.assert_has_calls(( + mock.call().write(b'1'), + )) + + +def test_read(gpio, patch_open): + pin = gpio.GPIOPin(10, gpio.IN) + + patch_open().read.return_value = b'1\n' + assert pin.read() == gpio.HIGH + assert gpio.read(10) == gpio.HIGH + + patch_open().read.return_value = b'0\n' + assert pin.read() == gpio.LOW + assert gpio.read(10) == gpio.LOW diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..113360a --- /dev/null +++ b/tox.ini @@ -0,0 +1,24 @@ +[tox] +envlist = py{27,35,37,39},qa +skip_missing_interpreters = True + +[testenv] +commands = + python setup.py install + coverage run -m py.test -v -r wsx + coverage report +deps = + mock + pytest>=3.1 + pytest-cov + +[testenv:qa] +commands = + check-manifest --ignore tox.ini,tests/*,.coveragerc + python setup.py sdist bdist_wheel + twine check dist/* + flake8 +deps = + check-manifest + flake8 + twine \ No newline at end of file