From d6df8e5726dd6ffaf01cd2049ccea0b5371bfbdc Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 21 Nov 2019 18:09:05 +0000 Subject: [PATCH 01/18] Update project to match cookiecutter-mopidy-ext --- .circleci/config.yml | 51 +++++++++++++++++++++++++ .gitignore | 16 ++++---- .travis.yml | 28 -------------- MANIFEST.in | 14 +++++-- README.rst | 16 ++++---- pyproject.toml | 17 +++++++++ setup.cfg | 89 +++++++++++++++++++++++++++++++++++++++++--- setup.py | 46 +---------------------- tox.ini | 30 +++++++-------- 9 files changed, 193 insertions(+), 114 deletions(-) create mode 100644 .circleci/config.yml delete mode 100644 .travis.yml create mode 100644 pyproject.toml diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..a5486e6 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,51 @@ +version: 2.1 + +orbs: + codecov: codecov/codecov@1.0.5 + +workflows: + version: 2 + test: + jobs: + - py38 + - py37 + - black + - check-manifest + - flake8 + +jobs: + py38: &test-template + docker: + - image: mopidy/ci-python:3.8 + steps: + - checkout + - restore_cache: + name: Restoring tox cache + {% raw %}key: tox-v1-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.cfg" }}{% endraw %} + - run: + name: Run tests + command: | + tox -e $CIRCLE_JOB -- \ + --junit-xml=test-results/pytest/results.xml \ + --cov-report=xml + - save_cache: + name: Saving tox cache + {% raw %}key: tox-v1-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.cfg" }}{% endraw %} + paths: + - ./.tox + - ~/.cache/pip + - codecov/upload: + file: coverage.xml + - store_test_results: + path: test-results + + py37: + <<: *test-template + docker: + - image: mopidy/ci-python:3.7 + + black: *test-template + + check-manifest: *test-template + + flake8: *test-template \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3b29847..5f0c41c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,9 @@ -*.egg-info *.pyc -.coverage -.pytest_cache/ -.tox/ -MANIFEST -build/ -dist/ -.vscode/ +/.coverage +/.mypy_cache/ +/.pytest_cache/ +/.tox/ +/*.egg-info +/build/ +/dist/ +/MANIFEST \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 32df99f..0000000 --- a/.travis.yml +++ /dev/null @@ -1,28 +0,0 @@ -language: python - -python: - - "2.7" - -virtualenv: - system_site_packages: true - -addons: - apt: - sources: - - mopidy-stable - packages: - - mopidy - -env: - - TOX_ENV=py27 - - TOX_ENV=flake8 - - TOX_ENV=check-manifest - -install: - - pip install tox - -script: - - tox -e $TOX_ENV - -after_success: - - if [ $TOX_ENV == 'py27' ]; then pip install coveralls; coveralls; fi diff --git a/MANIFEST.in b/MANIFEST.in index 31fe726..1ac4cab 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,9 +1,15 @@ -include .travis.yml -include CHANGELOG.rst +include *.py +include *.rst +include .mailmap include LICENSE include MANIFEST.in -include README.rst -include mopidy_raspberry_gpio/ext.conf +include pyproject.toml include tox.ini +recursive-include .circleci * +recursive-include .github * + +include mopidy_*/ext.conf + recursive-include tests *.py +recursive-include tests/data * \ No newline at end of file diff --git a/README.rst b/README.rst index 4e98f17..a8bb1ab 100644 --- a/README.rst +++ b/README.rst @@ -2,17 +2,17 @@ Mopidy-Raspberry-GPIO **************************** -.. image:: https://img.shields.io/pypi/v/Mopidy-Raspberry-GPIO.svg?style=flat +.. image:: https://img.shields.io/pypi/v/Mopidy-Raspberry-GPIO.svg :target: https://pypi.org/project/Mopidy-Raspberry-GPIO/ :alt: Latest PyPI version -.. image:: https://img.shields.io/travis/pimoroni/mopidy-raspberry-gpio/master.svg?style=flat - :target: https://travis-ci.org/pimoroni/mopidy-raspberry-gpio - :alt: Travis CI build status +.. image:: https://img.shields.io/circleci/build/gh/pimoroni/mopidy-raspberry-gpio + :target: https://circleci.com/gh/pimoroni/mopidy-raspberry-gpio + :alt: CircleCI build status -.. image:: https://img.shields.io/coveralls/pimoroni/mopidy-raspberry-gpio/master.svg?style=flat - :target: https://coveralls.io/r/pimoroni/mopidy-raspberry-gpio - :alt: Test coverage +.. image:: https://img.shields.io/codecov/c/gh/pimoroni/mopidy-raspberry-gpio + :target: https://codecov.io/gh/pimoroni/mopidy-raspberry-gpio + :alt: Test coverage Mopidy extension for GPIO input on a Raspberry Pi @@ -22,7 +22,7 @@ Installation Install by running:: - pip install Mopidy-Raspberry-GPIO + python3 -m pip install Mopidy-Raspberry-GPIO Or, if available, install the Debian/Ubuntu package from `apt.mopidy.com `_. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6a8423f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,17 @@ +[build-system] +requires = ["setuptools >= 30.3.0", "wheel"] + + +[tool.black] +target-version = ["py37", "py38"] +line-length = 80 + + +[tool.isort] +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true +line_length = 88 +known_tests = "tests" +sections = "FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,TESTS,LOCALFOLDER" \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 614c20c..b2ad4f8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,85 @@ -[flake8] -application-import-names = mopidy_raspberry_gpio,tests -exclude = .git,.tox +[metadata] +name = mopidy-raspberry-gpio +version = 0.0.2 +url = https://github.com/pimoroni/mopidy-raspberry-gpio +author = Phil Howard +author_email = phil@pimoroni.com +license = Apache License, Version 2.0 +license_file = LICENSE +description = Mopidy extension for GPIO input on a Raspberry Pi +long_description = file: README.rst +classifiers = + Environment :: No Input/Output (Daemon) + Intended Audience :: End Users/Desktop + License :: OSI Approved :: Apache Software License + Operating System :: OS Independent + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Topic :: Multimedia :: Sound/Audio :: Players -[wheel] -universal = 1 + +[options] +zip_safe = False +include_package_data = True +packages = find: +python_requires = >= 3.7 +install_requires = + Mopidy >= 3.0.0a4 # Change to >= 3.0 once final is released + Pykka >= 2.0.1 + setuptools + + +[options.extras_require] +lint = + black + check-manifest + flake8 + flake8-bugbear + flake8-import-order + isort[pyproject] +release = + twine + wheel +test = + pytest + pytest-cov +dev = + %(lint)s + %(release)s + %(test)s + + +[options.packages.find] +exclude = + tests + tests.* + + +[options.entry_points] +mopidy.ext = + raspberry-gpio = mopidy_raspberry_gpio:Extension + + +[flake8] +application-import-names = mopidy_raspberry_gpio, tests +max-line-length = 80 +exclude = .git, .tox, build +select = + # Regular flake8 rules + C, E, F, W + # flake8-bugbear rules + B + # B950: line too long (soft speed limit) + B950 + # pep8-naming rules + N +ignore = + # E203: whitespace before ':' (not PEP8 compliant) + E203 + # E501: line too long (replaced by B950) + E501 + # W503: line break before binary operator (not PEP8 compliant) + W503 + # B305: .next() is not a thing on Python 3 (used by playback controller) + B305 \ No newline at end of file diff --git a/setup.py b/setup.py index cbbc9b2..fc1f76c 100644 --- a/setup.py +++ b/setup.py @@ -1,45 +1,3 @@ -from __future__ import unicode_literals +from setuptools import setup -import re - -from setuptools import find_packages, setup - - -def get_version(filename): - with open(filename) as fh: - metadata = dict(re.findall('__([a-z]+)__ = "([^"]+)"', fh.read())) - return metadata["version"] - - -setup( - name="Mopidy-Raspberry-GPIO", - version=get_version("mopidy_raspberry_gpio/__init__.py"), - url="https://github.com/pimoroni/mopidy-raspberry-gpio", - license="Apache License, Version 2.0", - author="Phil Howard", - author_email="phil@pimoroni.com", - description="Mopidy extension for GPIO input on a Raspberry Pi", - long_description=open("README.rst").read(), - packages=find_packages(exclude=["tests", "tests.*"]), - zip_safe=False, - include_package_data=True, - python_requires="> 2.7, < 3", - install_requires=[ - "setuptools", - "Mopidy >= 2.2", - "Pykka >= 2.0", - ], - entry_points={ - "mopidy.ext": [ - "raspberry-gpio = mopidy_raspberry_gpio:Extension", - ] - }, - classifiers=[ - "Environment :: No Input/Output (Daemon)", - "Intended Audience :: End Users/Desktop", - "License :: OSI Approved :: Apache Software License", - "Operating System :: OS Independent", - "Programming Language :: Python :: 2.7", - "Topic :: Multimedia :: Sound/Audio :: Players", - ], -) +setup() \ No newline at end of file diff --git a/tox.ini b/tox.ini index 1e07a84..d57a105 100644 --- a/tox.ini +++ b/tox.ini @@ -1,27 +1,23 @@ [tox] -envlist = py27, py27-flake8, py27-check-manifest +envlist = py37, py38, black, check-manifest, flake8 [testenv] sitepackages = true -deps = - mock - pytest - pytest-cov - pytest-xdist +deps = .[test] commands = - pytest \ - -v -r wsx \ + python -m pytest \ --basetemp={envtmpdir} \ --cov=mopidy_raspberry_gpio --cov-report=term-missing \ {posargs} -[testenv:py27-flake8] -deps = - flake8 - flake8-import-order -skip_install = true -commands = python -m flake8 +[testenv:black] +deps = .[lint] +commands = python -m black --check . -[testenv:py27-check-manifest] -deps = check-manifest -commands = check-manifest +[testenv:check-manifest] +deps = .[lint] +commands = python -m check_manifest + +[testenv:flake8] +deps = .[lint] +commands = python -m flake8 --show-source --statistics \ No newline at end of file From ba32cf7df882fa7492b1da0a60c65a70bd339212 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 21 Nov 2019 18:11:43 +0000 Subject: [PATCH 02/18] pyupgrade --py37-plus **/*.py --- mopidy_raspberry_gpio/__init__.py | 6 ++---- mopidy_raspberry_gpio/frontend.py | 8 +++----- mopidy_raspberry_gpio/pinconfig.py | 2 +- tests/test_config.py | 2 -- tests/test_frontend.py | 2 -- 5 files changed, 6 insertions(+), 14 deletions(-) diff --git a/mopidy_raspberry_gpio/__init__.py b/mopidy_raspberry_gpio/__init__.py index 2187c7f..f449cb7 100644 --- a/mopidy_raspberry_gpio/__init__.py +++ b/mopidy_raspberry_gpio/__init__.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import logging import os @@ -24,9 +22,9 @@ class Extension(ext.Extension): return config.read(conf_file) def get_config_schema(self): - schema = super(Extension, self).get_config_schema() + schema = super().get_config_schema() for pin in range(28): - schema["bcm{:d}".format(pin)] = PinConfig() + schema[f"bcm{pin:d}"] = PinConfig() return schema def setup(self, registry): diff --git a/mopidy_raspberry_gpio/frontend.py b/mopidy_raspberry_gpio/frontend.py index 829f0b7..2be9e50 100644 --- a/mopidy_raspberry_gpio/frontend.py +++ b/mopidy_raspberry_gpio/frontend.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import logging from mopidy import core @@ -12,7 +10,7 @@ logger = logging.getLogger(__name__) class RaspberryGPIOFrontend(pykka.ThreadingActor, core.CoreListener): def __init__(self, config, core): - super(RaspberryGPIOFrontend, self).__init__() + super().__init__() import RPi.GPIO as GPIO self.core = core self.config = config["raspberry-gpio"] @@ -54,12 +52,12 @@ class RaspberryGPIOFrontend(pykka.ThreadingActor, core.CoreListener): self.dispatch_input(settings.event) def dispatch_input(self, event): - handler_name = "handle_{}".format(event) + handler_name = f"handle_{event}" try: getattr(self, handler_name)() except AttributeError: raise RuntimeError( - "Could not find input handler for event: {}".format(event) + f"Could not find input handler for event: {event}" ) def handle_play_pause(self): diff --git a/mopidy_raspberry_gpio/pinconfig.py b/mopidy_raspberry_gpio/pinconfig.py index e8a8942..9e59ebc 100644 --- a/mopidy_raspberry_gpio/pinconfig.py +++ b/mopidy_raspberry_gpio/pinconfig.py @@ -43,7 +43,7 @@ class PinConfig(config.ConfigValue): bouncetime = int(bouncetime) except ValueError: raise ValueError( - "invalid bouncetime value for pin config {}".format(bouncetime) + f"invalid bouncetime value for pin config {bouncetime}" ) return self.tuple_pinconfig(event, active, bouncetime) diff --git a/tests/test_config.py b/tests/test_config.py index c363999..467a8c7 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import pytest from mopidy_raspberry_gpio import Extension, PinConfig diff --git a/tests/test_frontend.py b/tests/test_frontend.py index 60105b8..f3a1f3f 100644 --- a/tests/test_frontend.py +++ b/tests/test_frontend.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import sys import mock From 87e0d912015fb19ae4dab243b9231ff147388a35 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 21 Nov 2019 18:13:31 +0000 Subject: [PATCH 03/18] black . --- mopidy_raspberry_gpio/__init__.py | 1 + mopidy_raspberry_gpio/frontend.py | 11 +++++------ mopidy_raspberry_gpio/pinconfig.py | 8 ++++---- setup.py | 2 +- tests/test_frontend.py | 13 +++++++------ 5 files changed, 18 insertions(+), 17 deletions(-) diff --git a/mopidy_raspberry_gpio/__init__.py b/mopidy_raspberry_gpio/__init__.py index f449cb7..99adcb6 100644 --- a/mopidy_raspberry_gpio/__init__.py +++ b/mopidy_raspberry_gpio/__init__.py @@ -29,4 +29,5 @@ class Extension(ext.Extension): def setup(self, registry): from .frontend import RaspberryGPIOFrontend + registry.add("frontend", RaspberryGPIOFrontend) diff --git a/mopidy_raspberry_gpio/frontend.py b/mopidy_raspberry_gpio/frontend.py index 2be9e50..fb730fd 100644 --- a/mopidy_raspberry_gpio/frontend.py +++ b/mopidy_raspberry_gpio/frontend.py @@ -12,6 +12,7 @@ class RaspberryGPIOFrontend(pykka.ThreadingActor, core.CoreListener): def __init__(self, config, core): super().__init__() import RPi.GPIO as GPIO + self.core = core self.config = config["raspberry-gpio"] self.pin_settings = {} @@ -30,20 +31,18 @@ class RaspberryGPIOFrontend(pykka.ThreadingActor, core.CoreListener): pull = GPIO.PUD_UP edge = GPIO.FALLING - if settings.active == 'active_high': + if settings.active == "active_high": pull = GPIO.PUD_DOWN edge = GPIO.RISING - GPIO.setup( - pin, - GPIO.IN, - pull_up_down=pull) + GPIO.setup(pin, GPIO.IN, pull_up_down=pull) GPIO.add_event_detect( pin, edge, callback=self.gpio_event, - bouncetime=settings.bouncetime) + bouncetime=settings.bouncetime, + ) self.pin_settings[pin] = settings diff --git a/mopidy_raspberry_gpio/pinconfig.py b/mopidy_raspberry_gpio/pinconfig.py index 9e59ebc..9ed9e9c 100644 --- a/mopidy_raspberry_gpio/pinconfig.py +++ b/mopidy_raspberry_gpio/pinconfig.py @@ -4,8 +4,7 @@ from mopidy import config class PinConfig(config.ConfigValue): - tuple_pinconfig = namedtuple("PinConfig", - ("event", "active", "bouncetime")) + tuple_pinconfig = namedtuple("PinConfig", ("event", "active", "bouncetime")) valid_events = "play_pause", "prev", "next", "volume_up", "volume_down" @@ -21,7 +20,7 @@ class PinConfig(config.ConfigValue): value = config.decode(value).strip() try: - event, active, bouncetime = value.split(',') + event, active, bouncetime = value.split(",") except ValueError: return None @@ -52,5 +51,6 @@ class PinConfig(config.ConfigValue): if value is None: return "" value = "{:s},{:s},{:d}".format( - value.event, value.active, value.bouncetime) + value.event, value.active, value.bouncetime + ) return config.encode(value) diff --git a/setup.py b/setup.py index fc1f76c..6068493 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,3 @@ from setuptools import setup -setup() \ No newline at end of file +setup() diff --git a/tests/test_frontend.py b/tests/test_frontend.py index f3a1f3f..8133fcb 100644 --- a/tests/test_frontend.py +++ b/tests/test_frontend.py @@ -19,8 +19,8 @@ dummy_config = { def test_get_frontend_classes(): - sys.modules['RPi'] = mock.Mock() - sys.modules['RPi.GPIO'] = mock.Mock() + sys.modules["RPi"] = mock.Mock() + sys.modules["RPi.GPIO"] = mock.Mock() ext = Extension() registry = mock.Mock() @@ -28,14 +28,15 @@ def test_get_frontend_classes(): ext.setup(registry) registry.add.assert_called_once_with( - 'frontend', frontend_lib.RaspberryGPIOFrontend) + "frontend", frontend_lib.RaspberryGPIOFrontend + ) def test_frontend_handler_dispatch(): - sys.modules['RPi'] = mock.Mock() - sys.modules['RPi.GPIO'] = mock.Mock() + sys.modules["RPi"] = mock.Mock() + sys.modules["RPi.GPIO"] = mock.Mock() frontend = frontend_lib.RaspberryGPIOFrontend(dummy_config, mock.Mock()) with pytest.raises(RuntimeError): - frontend.dispatch_input('tomato') + frontend.dispatch_input("tomato") From eacc95c9d7a3df5bd469162efb794a2f50029174 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 21 Nov 2019 18:14:34 +0000 Subject: [PATCH 04/18] isort -rc . && black . --- mopidy_raspberry_gpio/frontend.py | 4 +--- tests/test_config.py | 1 - tests/test_frontend.py | 6 ++---- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/mopidy_raspberry_gpio/frontend.py b/mopidy_raspberry_gpio/frontend.py index fb730fd..c3dd98d 100644 --- a/mopidy_raspberry_gpio/frontend.py +++ b/mopidy_raspberry_gpio/frontend.py @@ -1,9 +1,7 @@ import logging -from mopidy import core - import pykka - +from mopidy import core logger = logging.getLogger(__name__) diff --git a/tests/test_config.py b/tests/test_config.py index 467a8c7..0ca5765 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,5 +1,4 @@ import pytest - from mopidy_raspberry_gpio import Extension, PinConfig diff --git a/tests/test_frontend.py b/tests/test_frontend.py index 8133fcb..c7ead2a 100644 --- a/tests/test_frontend.py +++ b/tests/test_frontend.py @@ -1,12 +1,10 @@ import sys import mock - import pytest - -from mopidy_raspberry_gpio import Extension, pinconfig +from mopidy_raspberry_gpio import Extension from mopidy_raspberry_gpio import frontend as frontend_lib - +from mopidy_raspberry_gpio import pinconfig deserialize = pinconfig.PinConfig().deserialize From 610ff895fd63d18cc6c0849a910bde16a4bb8175 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 21 Nov 2019 18:16:56 +0000 Subject: [PATCH 05/18] Use pathlib in Extension --- mopidy_raspberry_gpio/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mopidy_raspberry_gpio/__init__.py b/mopidy_raspberry_gpio/__init__.py index 99adcb6..859f10a 100644 --- a/mopidy_raspberry_gpio/__init__.py +++ b/mopidy_raspberry_gpio/__init__.py @@ -1,5 +1,5 @@ import logging -import os +import pathlib from mopidy import config, ext @@ -18,8 +18,7 @@ class Extension(ext.Extension): version = __version__ def get_default_config(self): - conf_file = os.path.join(os.path.dirname(__file__), "ext.conf") - return config.read(conf_file) + return config.read(pathlib.Path(__file__).parent / "ext.conf") def get_config_schema(self): schema = super().get_config_schema() From 145fbf38d823a7d27f03e0555cde5107c3ce5c20 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 21 Nov 2019 18:24:41 +0000 Subject: [PATCH 06/18] Use unittest.mock instead of mock --- tests/test_frontend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_frontend.py b/tests/test_frontend.py index c7ead2a..ef9f65a 100644 --- a/tests/test_frontend.py +++ b/tests/test_frontend.py @@ -1,6 +1,6 @@ import sys +from unittest import mock -import mock import pytest from mopidy_raspberry_gpio import Extension from mopidy_raspberry_gpio import frontend as frontend_lib From 0bc4d3a030ff933165aae74db8c327281ae65318 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 21 Nov 2019 18:31:01 +0000 Subject: [PATCH 07/18] Use f-strings instead of % and .format() --- mopidy_raspberry_gpio/pinconfig.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/mopidy_raspberry_gpio/pinconfig.py b/mopidy_raspberry_gpio/pinconfig.py index 9ed9e9c..5f300cb 100644 --- a/mopidy_raspberry_gpio/pinconfig.py +++ b/mopidy_raspberry_gpio/pinconfig.py @@ -26,16 +26,12 @@ class PinConfig(config.ConfigValue): if event not in self.valid_events: raise ValueError( - "invalid event for pin config {:s} (Must be {})".format( - event, ", ".join(self.valid_events) - ) + f"invalid event for pin config {event} (Must be {', '.join(self.valid_events)})" ) if active not in self.valid_modes: raise ValueError( - "invalid mode for pin config {:s} (Must be {})".format( - event, ", ".join(self.valid_events) - ) + f"invalid event for pin config {active} (Must be {', '.join(self.valid_modes)})" ) try: @@ -50,7 +46,5 @@ class PinConfig(config.ConfigValue): def serialize(self, value, display=False): if value is None: return "" - value = "{:s},{:s},{:d}".format( - value.event, value.active, value.bouncetime - ) + value = f"{value.event},{value.active},{value.bouncetime}" return config.encode(value) From eadc6248df337843a9be77a284a4a1678d074da7 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 21 Nov 2019 19:04:13 +0000 Subject: [PATCH 08/18] Fix pinconfig encode,decode and linting --- mopidy_raspberry_gpio/pinconfig.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/mopidy_raspberry_gpio/pinconfig.py b/mopidy_raspberry_gpio/pinconfig.py index 5f300cb..100248d 100644 --- a/mopidy_raspberry_gpio/pinconfig.py +++ b/mopidy_raspberry_gpio/pinconfig.py @@ -1,14 +1,23 @@ + from collections import namedtuple from mopidy import config +from mopidy.config import types + + +class ValidList(list): + def __format__(self, format_string=None): + if format_string is None: + format_string = ", " + return format_string.join(self) class PinConfig(config.ConfigValue): tuple_pinconfig = namedtuple("PinConfig", ("event", "active", "bouncetime")) - valid_events = "play_pause", "prev", "next", "volume_up", "volume_down" + valid_events = ValidList(["play_pause", "prev", "next", "volume_up", "volume_down"]) - valid_modes = "active_low", "active_high" + valid_modes = ValidList(["active_low", "active_high"]) def __init__(self): pass @@ -17,7 +26,7 @@ class PinConfig(config.ConfigValue): if value is None: return None - value = config.decode(value).strip() + value = types.decode(value).strip() try: event, active, bouncetime = value.split(",") @@ -26,12 +35,12 @@ class PinConfig(config.ConfigValue): if event not in self.valid_events: raise ValueError( - f"invalid event for pin config {event} (Must be {', '.join(self.valid_events)})" + f"invalid event for pin config {event} (Must be {valid_events})" ) if active not in self.valid_modes: raise ValueError( - f"invalid event for pin config {active} (Must be {', '.join(self.valid_modes)})" + f"invalid event for pin config {active} (Must be one of {valid_modes})" ) try: @@ -47,4 +56,4 @@ class PinConfig(config.ConfigValue): if value is None: return "" value = f"{value.event},{value.active},{value.bouncetime}" - return config.encode(value) + return types.encode(value) From 68abdf508b5fc955365526b5039e7f86b77e6ecc Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 21 Nov 2019 19:06:13 +0000 Subject: [PATCH 09/18] dos2unix crlf -> lf --- mopidy_raspberry_gpio/frontend.py | 164 ++++++++++++++--------------- mopidy_raspberry_gpio/pinconfig.py | 118 ++++++++++----------- tests/test_frontend.py | 80 +++++++------- 3 files changed, 181 insertions(+), 181 deletions(-) diff --git a/mopidy_raspberry_gpio/frontend.py b/mopidy_raspberry_gpio/frontend.py index c3dd98d..9e2a3db 100644 --- a/mopidy_raspberry_gpio/frontend.py +++ b/mopidy_raspberry_gpio/frontend.py @@ -1,82 +1,82 @@ -import logging - -import pykka -from mopidy import core - -logger = logging.getLogger(__name__) - - -class RaspberryGPIOFrontend(pykka.ThreadingActor, core.CoreListener): - def __init__(self, config, core): - super().__init__() - import RPi.GPIO as GPIO - - self.core = core - self.config = config["raspberry-gpio"] - self.pin_settings = {} - - GPIO.setwarnings(False) - GPIO.setmode(GPIO.BCM) - - # Iterate through any bcmN pins in the config - # and set them up as inputs with edge detection - for key in self.config: - if key.startswith("bcm"): - pin = int(key.replace("bcm", "")) - settings = self.config[key] - if settings is None: - continue - - pull = GPIO.PUD_UP - edge = GPIO.FALLING - if settings.active == "active_high": - pull = GPIO.PUD_DOWN - edge = GPIO.RISING - - GPIO.setup(pin, GPIO.IN, pull_up_down=pull) - - GPIO.add_event_detect( - pin, - edge, - callback=self.gpio_event, - bouncetime=settings.bouncetime, - ) - - self.pin_settings[pin] = settings - - def gpio_event(self, pin): - settings = self.pin_settings[pin] - self.dispatch_input(settings.event) - - def dispatch_input(self, event): - handler_name = f"handle_{event}" - try: - getattr(self, handler_name)() - except AttributeError: - raise RuntimeError( - f"Could not find input handler for event: {event}" - ) - - def handle_play_pause(self): - if self.core.playback.state.get() == core.PlaybackState.PLAYING: - self.core.playback.pause() - else: - self.core.playback.play() - - def handle_next(self): - self.core.playback.next() - - def handle_prev(self): - self.core.playback.previous() - - def handle_volume_up(self): - volume = self.core.playback.volume.get() - volume += 5 - volume = min(volume, 100) - self.core.playback.volume = volume - - def handle_volume_down(self): - volume = self.core.playback.volume.get() - volume -= 5 - volume = max(volume, 0) - self.core.playback.volume = volume +import logging + +import pykka +from mopidy import core + +logger = logging.getLogger(__name__) + + +class RaspberryGPIOFrontend(pykka.ThreadingActor, core.CoreListener): + def __init__(self, config, core): + super().__init__() + import RPi.GPIO as GPIO + + self.core = core + self.config = config["raspberry-gpio"] + self.pin_settings = {} + + GPIO.setwarnings(False) + GPIO.setmode(GPIO.BCM) + + # Iterate through any bcmN pins in the config + # and set them up as inputs with edge detection + for key in self.config: + if key.startswith("bcm"): + pin = int(key.replace("bcm", "")) + settings = self.config[key] + if settings is None: + continue + + pull = GPIO.PUD_UP + edge = GPIO.FALLING + if settings.active == "active_high": + pull = GPIO.PUD_DOWN + edge = GPIO.RISING + + GPIO.setup(pin, GPIO.IN, pull_up_down=pull) + + GPIO.add_event_detect( + pin, + edge, + callback=self.gpio_event, + bouncetime=settings.bouncetime, + ) + + self.pin_settings[pin] = settings + + def gpio_event(self, pin): + settings = self.pin_settings[pin] + self.dispatch_input(settings.event) + + def dispatch_input(self, event): + handler_name = f"handle_{event}" + try: + getattr(self, handler_name)() + except AttributeError: + raise RuntimeError( + f"Could not find input handler for event: {event}" + ) + + def handle_play_pause(self): + if self.core.playback.state.get() == core.PlaybackState.PLAYING: + self.core.playback.pause() + else: + self.core.playback.play() + + def handle_next(self): + self.core.playback.next() + + def handle_prev(self): + self.core.playback.previous() + + def handle_volume_up(self): + volume = self.core.playback.volume.get() + volume += 5 + volume = min(volume, 100) + self.core.playback.volume = volume + + def handle_volume_down(self): + volume = self.core.playback.volume.get() + volume -= 5 + volume = max(volume, 0) + self.core.playback.volume = volume diff --git a/mopidy_raspberry_gpio/pinconfig.py b/mopidy_raspberry_gpio/pinconfig.py index 100248d..8e5c93e 100644 --- a/mopidy_raspberry_gpio/pinconfig.py +++ b/mopidy_raspberry_gpio/pinconfig.py @@ -1,59 +1,59 @@ - -from collections import namedtuple - -from mopidy import config -from mopidy.config import types - - -class ValidList(list): - def __format__(self, format_string=None): - if format_string is None: - format_string = ", " - return format_string.join(self) - - -class PinConfig(config.ConfigValue): - tuple_pinconfig = namedtuple("PinConfig", ("event", "active", "bouncetime")) - - valid_events = ValidList(["play_pause", "prev", "next", "volume_up", "volume_down"]) - - valid_modes = ValidList(["active_low", "active_high"]) - - def __init__(self): - pass - - def deserialize(self, value): - if value is None: - return None - - value = types.decode(value).strip() - - try: - event, active, bouncetime = value.split(",") - except ValueError: - return None - - if event not in self.valid_events: - raise ValueError( - f"invalid event for pin config {event} (Must be {valid_events})" - ) - - if active not in self.valid_modes: - raise ValueError( - f"invalid event for pin config {active} (Must be one of {valid_modes})" - ) - - try: - bouncetime = int(bouncetime) - except ValueError: - raise ValueError( - f"invalid bouncetime value for pin config {bouncetime}" - ) - - return self.tuple_pinconfig(event, active, bouncetime) - - def serialize(self, value, display=False): - if value is None: - return "" - value = f"{value.event},{value.active},{value.bouncetime}" - return types.encode(value) + +from collections import namedtuple + +from mopidy import config +from mopidy.config import types + + +class ValidList(list): + def __format__(self, format_string=None): + if format_string is None: + format_string = ", " + return format_string.join(self) + + +class PinConfig(config.ConfigValue): + tuple_pinconfig = namedtuple("PinConfig", ("event", "active", "bouncetime")) + + valid_events = ValidList(["play_pause", "prev", "next", "volume_up", "volume_down"]) + + valid_modes = ValidList(["active_low", "active_high"]) + + def __init__(self): + pass + + def deserialize(self, value): + if value is None: + return None + + value = types.decode(value).strip() + + try: + event, active, bouncetime = value.split(",") + except ValueError: + return None + + if event not in self.valid_events: + raise ValueError( + f"invalid event for pin config {event} (Must be {valid_events})" + ) + + if active not in self.valid_modes: + raise ValueError( + f"invalid event for pin config {active} (Must be one of {valid_modes})" + ) + + try: + bouncetime = int(bouncetime) + except ValueError: + raise ValueError( + f"invalid bouncetime value for pin config {bouncetime}" + ) + + return self.tuple_pinconfig(event, active, bouncetime) + + def serialize(self, value, display=False): + if value is None: + return "" + value = f"{value.event},{value.active},{value.bouncetime}" + return types.encode(value) diff --git a/tests/test_frontend.py b/tests/test_frontend.py index ef9f65a..c2834ce 100644 --- a/tests/test_frontend.py +++ b/tests/test_frontend.py @@ -1,40 +1,40 @@ -import sys -from unittest import mock - -import pytest -from mopidy_raspberry_gpio import Extension -from mopidy_raspberry_gpio import frontend as frontend_lib -from mopidy_raspberry_gpio import pinconfig - -deserialize = pinconfig.PinConfig().deserialize - -dummy_config = { - "raspberry-gpio": { - # Plugins expect settings to be deserialized - "bcm1": deserialize("play_pause,active_low,30") - } -} - - -def test_get_frontend_classes(): - sys.modules["RPi"] = mock.Mock() - sys.modules["RPi.GPIO"] = mock.Mock() - - ext = Extension() - registry = mock.Mock() - - ext.setup(registry) - - registry.add.assert_called_once_with( - "frontend", frontend_lib.RaspberryGPIOFrontend - ) - - -def test_frontend_handler_dispatch(): - sys.modules["RPi"] = mock.Mock() - sys.modules["RPi.GPIO"] = mock.Mock() - - frontend = frontend_lib.RaspberryGPIOFrontend(dummy_config, mock.Mock()) - - with pytest.raises(RuntimeError): - frontend.dispatch_input("tomato") +import sys +from unittest import mock + +import pytest +from mopidy_raspberry_gpio import Extension +from mopidy_raspberry_gpio import frontend as frontend_lib +from mopidy_raspberry_gpio import pinconfig + +deserialize = pinconfig.PinConfig().deserialize + +dummy_config = { + "raspberry-gpio": { + # Plugins expect settings to be deserialized + "bcm1": deserialize("play_pause,active_low,30") + } +} + + +def test_get_frontend_classes(): + sys.modules["RPi"] = mock.Mock() + sys.modules["RPi.GPIO"] = mock.Mock() + + ext = Extension() + registry = mock.Mock() + + ext.setup(registry) + + registry.add.assert_called_once_with( + "frontend", frontend_lib.RaspberryGPIOFrontend + ) + + +def test_frontend_handler_dispatch(): + sys.modules["RPi"] = mock.Mock() + sys.modules["RPi.GPIO"] = mock.Mock() + + frontend = frontend_lib.RaspberryGPIOFrontend(dummy_config, mock.Mock()) + + with pytest.raises(RuntimeError): + frontend.dispatch_input("tomato") From 01a4e792ad6d5026db1bfb064f4fba1770f19120 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 21 Nov 2019 19:54:46 +0000 Subject: [PATCH 10/18] Fix silly blunder --- mopidy_raspberry_gpio/pinconfig.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy_raspberry_gpio/pinconfig.py b/mopidy_raspberry_gpio/pinconfig.py index 8e5c93e..f2d0a7f 100644 --- a/mopidy_raspberry_gpio/pinconfig.py +++ b/mopidy_raspberry_gpio/pinconfig.py @@ -35,12 +35,12 @@ class PinConfig(config.ConfigValue): if event not in self.valid_events: raise ValueError( - f"invalid event for pin config {event} (Must be {valid_events})" + f"invalid event for pin config {event} (Must be {self.valid_events})" ) if active not in self.valid_modes: raise ValueError( - f"invalid event for pin config {active} (Must be one of {valid_modes})" + f"invalid event for pin config {active} (Must be one of {self.valid_modes})" ) try: From 6aa1e7a8b6f70d82653a23420e1837eadf2cad83 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 21 Nov 2019 20:02:43 +0000 Subject: [PATCH 11/18] Expand frontend test coverage --- tests/test_frontend.py | 60 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/tests/test_frontend.py b/tests/test_frontend.py index c2834ce..56607e9 100644 --- a/tests/test_frontend.py +++ b/tests/test_frontend.py @@ -11,7 +11,9 @@ deserialize = pinconfig.PinConfig().deserialize dummy_config = { "raspberry-gpio": { # Plugins expect settings to be deserialized - "bcm1": deserialize("play_pause,active_low,30") + "bcm1": deserialize("play_pause,active_low,30"), + "bcm2": deserialize("volume_up,active_high,30"), + "bcm3": deserialize("volume_down,active_high,30") } } @@ -30,7 +32,52 @@ def test_get_frontend_classes(): ) -def test_frontend_handler_dispatch(): +def test_frontend_handler_dispatch_play_pause(): + sys.modules["RPi"] = mock.Mock() + sys.modules["RPi.GPIO"] = mock.Mock() + + frontend = frontend_lib.RaspberryGPIOFrontend(dummy_config, mock.Mock()) + + frontend.dispatch_input("play_pause") + + +def test_frontend_handler_dispatch_next(): + sys.modules["RPi"] = mock.Mock() + sys.modules["RPi.GPIO"] = mock.Mock() + + frontend = frontend_lib.RaspberryGPIOFrontend(dummy_config, mock.Mock()) + + frontend.dispatch_input("next") + + +def test_frontend_handler_dispatch_prev(): + sys.modules["RPi"] = mock.Mock() + sys.modules["RPi.GPIO"] = mock.Mock() + + frontend = frontend_lib.RaspberryGPIOFrontend(dummy_config, mock.Mock()) + + frontend.dispatch_input("prev") + + +def test_frontend_handler_dispatch_volume_up(): + sys.modules["RPi"] = mock.Mock() + sys.modules["RPi.GPIO"] = mock.Mock() + + frontend = frontend_lib.RaspberryGPIOFrontend(dummy_config, mock.Mock()) + + frontend.dispatch_input("volume_up") + + +def test_frontend_handler_dispatch_volume_down(): + sys.modules["RPi"] = mock.Mock() + sys.modules["RPi.GPIO"] = mock.Mock() + + frontend = frontend_lib.RaspberryGPIOFrontend(dummy_config, mock.Mock()) + + frontend.dispatch_input("volume_down") + + +def test_frontend_handler_dispatch_invalid_event(): sys.modules["RPi"] = mock.Mock() sys.modules["RPi.GPIO"] = mock.Mock() @@ -38,3 +85,12 @@ def test_frontend_handler_dispatch(): with pytest.raises(RuntimeError): frontend.dispatch_input("tomato") + + +def test_frontend_gpio_event(): + sys.modules["RPi"] = mock.Mock() + sys.modules["RPi.GPIO"] = mock.Mock() + + frontend = frontend_lib.RaspberryGPIOFrontend(dummy_config, mock.Mock()) + + frontend.gpio_event(3) From 7e8c4411002b33bf408a1c5b0206fdca51908df5 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 21 Nov 2019 20:25:51 +0000 Subject: [PATCH 12/18] Remove cookiecutter raw markup --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a5486e6..6a76d5b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -21,7 +21,7 @@ jobs: - checkout - restore_cache: name: Restoring tox cache - {% raw %}key: tox-v1-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.cfg" }}{% endraw %} + key: tox-v1-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.cfg" }} - run: name: Run tests command: | @@ -30,7 +30,7 @@ jobs: --cov-report=xml - save_cache: name: Saving tox cache - {% raw %}key: tox-v1-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.cfg" }}{% endraw %} + key: tox-v1-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.cfg" }} paths: - ./.tox - ~/.cache/pip From 838156d2ffa377fe1d1fdd8b2b3bcfc1c2985f79 Mon Sep 17 00:00:00 2001 From: Philip Howard Date: Thu, 23 Jan 2020 11:49:37 +0000 Subject: [PATCH 13/18] Poke CircleCI to recompile config --- .circleci/config.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 6a76d5b..727ec15 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -48,4 +48,5 @@ jobs: check-manifest: *test-template - flake8: *test-template \ No newline at end of file + flake8: *test-template + From 5854f568fe5437d4a9ef892bd67061b6b3361ea4 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 23 Jan 2020 12:37:36 +0000 Subject: [PATCH 14/18] Fix for 3.x API changes Switches from use of playback controller (deprecated) to mixer volume as mentioned in #1 Uses `get_state()` instead of `state` --- mopidy_raspberry_gpio/frontend.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mopidy_raspberry_gpio/frontend.py b/mopidy_raspberry_gpio/frontend.py index 9e2a3db..1bbdf42 100644 --- a/mopidy_raspberry_gpio/frontend.py +++ b/mopidy_raspberry_gpio/frontend.py @@ -58,7 +58,7 @@ class RaspberryGPIOFrontend(pykka.ThreadingActor, core.CoreListener): ) def handle_play_pause(self): - if self.core.playback.state.get() == core.PlaybackState.PLAYING: + if self.core.playback.get_state().get() == core.PlaybackState.PLAYING: self.core.playback.pause() else: self.core.playback.play() @@ -70,13 +70,13 @@ class RaspberryGPIOFrontend(pykka.ThreadingActor, core.CoreListener): self.core.playback.previous() def handle_volume_up(self): - volume = self.core.playback.volume.get() + volume = self.core.mixer.get_volume().get() volume += 5 volume = min(volume, 100) - self.core.playback.volume = volume + self.core.mixer.set_volume(volume) def handle_volume_down(self): - volume = self.core.playback.volume.get() + volume = self.core.mixer.get_volume().get() volume -= 5 volume = max(volume, 0) - self.core.playback.volume = volume + self.core.mixer.set_volume(volume) From 07759149d60acd3104183acbbf6f64ff00ec65d8 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 23 Jan 2020 12:46:18 +0000 Subject: [PATCH 15/18] Apply black reformatting suggestions --- mopidy_raspberry_gpio/pinconfig.py | 5 +++-- tests/test_frontend.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/mopidy_raspberry_gpio/pinconfig.py b/mopidy_raspberry_gpio/pinconfig.py index f2d0a7f..f6c7270 100644 --- a/mopidy_raspberry_gpio/pinconfig.py +++ b/mopidy_raspberry_gpio/pinconfig.py @@ -1,4 +1,3 @@ - from collections import namedtuple from mopidy import config @@ -15,7 +14,9 @@ class ValidList(list): class PinConfig(config.ConfigValue): tuple_pinconfig = namedtuple("PinConfig", ("event", "active", "bouncetime")) - valid_events = ValidList(["play_pause", "prev", "next", "volume_up", "volume_down"]) + valid_events = ValidList( + ["play_pause", "prev", "next", "volume_up", "volume_down"] + ) valid_modes = ValidList(["active_low", "active_high"]) diff --git a/tests/test_frontend.py b/tests/test_frontend.py index 56607e9..427d4e6 100644 --- a/tests/test_frontend.py +++ b/tests/test_frontend.py @@ -13,7 +13,7 @@ dummy_config = { # Plugins expect settings to be deserialized "bcm1": deserialize("play_pause,active_low,30"), "bcm2": deserialize("volume_up,active_high,30"), - "bcm3": deserialize("volume_down,active_high,30") + "bcm3": deserialize("volume_down,active_high,30"), } } From c669f5e35c0871c8aaba25e8c9bcdd02080ab345 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 23 Jan 2020 13:35:08 +0000 Subject: [PATCH 16/18] Supplied dummy Mopidy Core to frontend for testing This currently uses three files copy-n-pasted from upstream Mopidy to provide dummy interfaces for mixer, backend and audio. These dummy files seem a little janky- should they be part of a mopidy-test package so that they stay in-sync with Mopidy's API? Since otherwise tests may?? pass that fail against the real Mopidy. Maybe? --- tests/dummy_audio.py | 138 +++++++++++++++++++++++++++++++++++++ tests/dummy_backend.py | 153 +++++++++++++++++++++++++++++++++++++++++ tests/dummy_mixer.py | 30 ++++++++ tests/test_frontend.py | 30 ++++++-- tox.ini | 2 +- 5 files changed, 345 insertions(+), 8 deletions(-) create mode 100644 tests/dummy_audio.py create mode 100644 tests/dummy_backend.py create mode 100644 tests/dummy_mixer.py diff --git a/tests/dummy_audio.py b/tests/dummy_audio.py new file mode 100644 index 0000000..144f1a4 --- /dev/null +++ b/tests/dummy_audio.py @@ -0,0 +1,138 @@ +"""A dummy audio actor for use in tests. + +This class implements the audio API in the simplest way possible. It is used in +tests of the core and backends. +""" + + +import pykka + +from mopidy import audio + + +def create_proxy(config=None, mixer=None): + return DummyAudio.start(config, mixer).proxy() + + +# TODO: reset position on track change? +class DummyAudio(pykka.ThreadingActor): + def __init__(self, config=None, mixer=None): + super().__init__() + self.state = audio.PlaybackState.STOPPED + self._volume = 0 + self._position = 0 + self._callback = None + self._uri = None + self._stream_changed = False + self._live_stream = False + self._tags = {} + self._bad_uris = set() + + def set_uri(self, uri, live_stream=False): + assert self._uri is None, "prepare change not called before set" + self._position = 0 + self._uri = uri + self._stream_changed = True + self._live_stream = live_stream + self._tags = {} + + def set_appsrc(self, *args, **kwargs): + pass + + def emit_data(self, buffer_): + pass + + def get_position(self): + return self._position + + def set_position(self, position): + self._position = position + audio.AudioListener.send("position_changed", position=position) + return True + + def start_playback(self): + return self._change_state(audio.PlaybackState.PLAYING) + + def pause_playback(self): + return self._change_state(audio.PlaybackState.PAUSED) + + def prepare_change(self): + self._uri = None + return True + + def stop_playback(self): + return self._change_state(audio.PlaybackState.STOPPED) + + def get_volume(self): + return self._volume + + def set_volume(self, volume): + self._volume = volume + return True + + def set_metadata(self, track): + pass + + def get_current_tags(self): + return self._tags + + def set_about_to_finish_callback(self, callback): + self._callback = callback + + def enable_sync_handler(self): + pass + + def wait_for_state_change(self): + pass + + def _change_state(self, new_state): + if not self._uri: + return False + + if new_state == audio.PlaybackState.STOPPED and self._uri: + self._stream_changed = True + self._uri = None + + if self._uri is not None: + audio.AudioListener.send("position_changed", position=0) + + if self._stream_changed: + self._stream_changed = False + audio.AudioListener.send("stream_changed", uri=self._uri) + + old_state, self.state = self.state, new_state + audio.AudioListener.send( + "state_changed", + old_state=old_state, + new_state=new_state, + target_state=None, + ) + + if new_state == audio.PlaybackState.PLAYING: + self._tags["audio-codec"] = ["fake info..."] + audio.AudioListener.send("tags_changed", tags=["audio-codec"]) + + return self._uri not in self._bad_uris + + def trigger_fake_playback_failure(self, uri): + self._bad_uris.add(uri) + + def trigger_fake_tags_changed(self, tags): + self._tags.update(tags) + audio.AudioListener.send("tags_changed", tags=self._tags.keys()) + + def get_about_to_finish_callback(self): + # This needs to be called from outside the actor or we lock up. + def wrapper(): + if self._callback: + self.prepare_change() + self._callback() + + if not self._uri or not self._callback: + self._tags = {} + audio.AudioListener.send("reached_end_of_stream") + else: + audio.AudioListener.send("position_changed", position=0) + audio.AudioListener.send("stream_changed", uri=self._uri) + + return wrapper diff --git a/tests/dummy_backend.py b/tests/dummy_backend.py new file mode 100644 index 0000000..0ee3d2d --- /dev/null +++ b/tests/dummy_backend.py @@ -0,0 +1,153 @@ +"""A dummy backend for use in tests. + +This backend implements the backend API in the simplest way possible. It is +used in tests of the frontends. +""" + + +import pykka + +from mopidy import backend +from mopidy.models import Playlist, Ref, SearchResult + + +def create_proxy(config=None, audio=None): + return DummyBackend.start(config=config, audio=audio).proxy() + + +class DummyBackend(pykka.ThreadingActor, backend.Backend): + def __init__(self, config, audio): + super().__init__() + + self.library = DummyLibraryProvider(backend=self) + if audio: + self.playback = backend.PlaybackProvider(audio=audio, backend=self) + else: + self.playback = DummyPlaybackProvider(audio=audio, backend=self) + self.playlists = DummyPlaylistsProvider(backend=self) + + self.uri_schemes = ["dummy"] + + +class DummyLibraryProvider(backend.LibraryProvider): + root_directory = Ref.directory(uri="dummy:/", name="dummy") + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.dummy_library = [] + self.dummy_get_distinct_result = {} + self.dummy_browse_result = {} + self.dummy_find_exact_result = SearchResult() + self.dummy_search_result = SearchResult() + + def browse(self, path): + return self.dummy_browse_result.get(path, []) + + def get_distinct(self, field, query=None): + return self.dummy_get_distinct_result.get(field, set()) + + def lookup(self, uri): + uri = Ref.track(uri=uri).uri + return [t for t in self.dummy_library if uri == t.uri] + + def refresh(self, uri=None): + pass + + def search(self, query=None, uris=None, exact=False): + if exact: # TODO: remove uses of dummy_find_exact_result + return self.dummy_find_exact_result + return self.dummy_search_result + + +class DummyPlaybackProvider(backend.PlaybackProvider): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._uri = None + self._time_position = 0 + + def pause(self): + return True + + def play(self): + return self._uri and self._uri != "dummy:error" + + def change_track(self, track): + """Pass a track with URI 'dummy:error' to force failure""" + self._uri = track.uri + self._time_position = 0 + return True + + def prepare_change(self): + pass + + def resume(self): + return True + + def seek(self, time_position): + self._time_position = time_position + return True + + def stop(self): + self._uri = None + return True + + def get_time_position(self): + return self._time_position + + +class DummyPlaylistsProvider(backend.PlaylistsProvider): + def __init__(self, backend): + super().__init__(backend) + self._playlists = [] + self._allow_save = True + + def set_dummy_playlists(self, playlists): + """For tests using the dummy provider through an actor proxy.""" + self._playlists = playlists + + def set_allow_save(self, enabled): + self._allow_save = enabled + + def as_list(self): + return [ + Ref.playlist(uri=pl.uri, name=pl.name) for pl in self._playlists + ] + + def get_items(self, uri): + playlist = self.lookup(uri) + if playlist is None: + return + return [Ref.track(uri=t.uri, name=t.name) for t in playlist.tracks] + + def lookup(self, uri): + uri = Ref.playlist(uri=uri).uri + for playlist in self._playlists: + if playlist.uri == uri: + return playlist + + def refresh(self): + pass + + def create(self, name): + playlist = Playlist(name=name, uri=f"dummy:{name}") + self._playlists.append(playlist) + return playlist + + def delete(self, uri): + playlist = self.lookup(uri) + if playlist: + self._playlists.remove(playlist) + + def save(self, playlist): + if not self._allow_save: + return None + + old_playlist = self.lookup(playlist.uri) + + if old_playlist is not None: + index = self._playlists.index(old_playlist) + self._playlists[index] = playlist + else: + self._playlists.append(playlist) + + return playlist diff --git a/tests/dummy_mixer.py b/tests/dummy_mixer.py new file mode 100644 index 0000000..f030ca2 --- /dev/null +++ b/tests/dummy_mixer.py @@ -0,0 +1,30 @@ +import pykka + +from mopidy import mixer + + +def create_proxy(config=None): + return DummyMixer.start(config=None).proxy() + + +class DummyMixer(pykka.ThreadingActor, mixer.Mixer): + def __init__(self, config): + super().__init__() + self._volume = 50 # Had to be initialised to avoid none type error in tests + self._mute = False # Ditto + + def get_volume(self): + return self._volume + + def set_volume(self, volume): + self._volume = volume + self.trigger_volume_changed(volume=volume) + return True + + def get_mute(self): + return self._mute + + def set_mute(self, mute): + self._mute = mute + self.trigger_mute_changed(mute=mute) + return True diff --git a/tests/test_frontend.py b/tests/test_frontend.py index 427d4e6..08c3442 100644 --- a/tests/test_frontend.py +++ b/tests/test_frontend.py @@ -6,6 +6,11 @@ from mopidy_raspberry_gpio import Extension from mopidy_raspberry_gpio import frontend as frontend_lib from mopidy_raspberry_gpio import pinconfig +from mopidy import core +from . import dummy_mixer +from . import dummy_backend +from . import dummy_audio + deserialize = pinconfig.PinConfig().deserialize dummy_config = { @@ -18,6 +23,17 @@ dummy_config = { } +def dummy_mopidy_core(): + mixer = dummy_mixer.create_proxy() + audio = dummy_audio.create_proxy() + backend = dummy_backend.create_proxy(audio=audio) + return core.Core.start( + audio=audio, + mixer=mixer, + backends=[backend] + ).proxy() + + def test_get_frontend_classes(): sys.modules["RPi"] = mock.Mock() sys.modules["RPi.GPIO"] = mock.Mock() @@ -36,7 +52,7 @@ def test_frontend_handler_dispatch_play_pause(): sys.modules["RPi"] = mock.Mock() sys.modules["RPi.GPIO"] = mock.Mock() - frontend = frontend_lib.RaspberryGPIOFrontend(dummy_config, mock.Mock()) + frontend = frontend_lib.RaspberryGPIOFrontend(dummy_config, dummy_mopidy_core()) frontend.dispatch_input("play_pause") @@ -45,7 +61,7 @@ def test_frontend_handler_dispatch_next(): sys.modules["RPi"] = mock.Mock() sys.modules["RPi.GPIO"] = mock.Mock() - frontend = frontend_lib.RaspberryGPIOFrontend(dummy_config, mock.Mock()) + frontend = frontend_lib.RaspberryGPIOFrontend(dummy_config, dummy_mopidy_core()) frontend.dispatch_input("next") @@ -54,7 +70,7 @@ def test_frontend_handler_dispatch_prev(): sys.modules["RPi"] = mock.Mock() sys.modules["RPi.GPIO"] = mock.Mock() - frontend = frontend_lib.RaspberryGPIOFrontend(dummy_config, mock.Mock()) + frontend = frontend_lib.RaspberryGPIOFrontend(dummy_config, dummy_mopidy_core()) frontend.dispatch_input("prev") @@ -63,7 +79,7 @@ def test_frontend_handler_dispatch_volume_up(): sys.modules["RPi"] = mock.Mock() sys.modules["RPi.GPIO"] = mock.Mock() - frontend = frontend_lib.RaspberryGPIOFrontend(dummy_config, mock.Mock()) + frontend = frontend_lib.RaspberryGPIOFrontend(dummy_config, dummy_mopidy_core()) frontend.dispatch_input("volume_up") @@ -72,7 +88,7 @@ def test_frontend_handler_dispatch_volume_down(): sys.modules["RPi"] = mock.Mock() sys.modules["RPi.GPIO"] = mock.Mock() - frontend = frontend_lib.RaspberryGPIOFrontend(dummy_config, mock.Mock()) + frontend = frontend_lib.RaspberryGPIOFrontend(dummy_config, dummy_mopidy_core()) frontend.dispatch_input("volume_down") @@ -81,7 +97,7 @@ def test_frontend_handler_dispatch_invalid_event(): sys.modules["RPi"] = mock.Mock() sys.modules["RPi.GPIO"] = mock.Mock() - frontend = frontend_lib.RaspberryGPIOFrontend(dummy_config, mock.Mock()) + frontend = frontend_lib.RaspberryGPIOFrontend(dummy_config, dummy_mopidy_core()) with pytest.raises(RuntimeError): frontend.dispatch_input("tomato") @@ -91,6 +107,6 @@ def test_frontend_gpio_event(): sys.modules["RPi"] = mock.Mock() sys.modules["RPi.GPIO"] = mock.Mock() - frontend = frontend_lib.RaspberryGPIOFrontend(dummy_config, mock.Mock()) + frontend = frontend_lib.RaspberryGPIOFrontend(dummy_config, dummy_mopidy_core()) frontend.gpio_event(3) diff --git a/tox.ini b/tox.ini index d57a105..d92eb3d 100644 --- a/tox.ini +++ b/tox.ini @@ -20,4 +20,4 @@ commands = python -m check_manifest [testenv:flake8] deps = .[lint] -commands = python -m flake8 --show-source --statistics \ No newline at end of file +commands = python -m flake8 --ignore=B950 --show-source --statistics From 844cead5ba4e0e1938254c1dfa6603189d8c8e24 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 23 Jan 2020 13:44:58 +0000 Subject: [PATCH 17/18] Black/Flake8 linting fixes --- tests/dummy_mixer.py | 5 +++-- tests/test_frontend.py | 34 ++++++++++++++++++++++------------ tox.ini | 2 +- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/tests/dummy_mixer.py b/tests/dummy_mixer.py index f030ca2..7cdffcc 100644 --- a/tests/dummy_mixer.py +++ b/tests/dummy_mixer.py @@ -10,8 +10,9 @@ def create_proxy(config=None): class DummyMixer(pykka.ThreadingActor, mixer.Mixer): def __init__(self, config): super().__init__() - self._volume = 50 # Had to be initialised to avoid none type error in tests - self._mute = False # Ditto + # These must be initialised to avoid none type error in tests + self._volume = 50 + self._mute = False def get_volume(self): return self._volume diff --git a/tests/test_frontend.py b/tests/test_frontend.py index 08c3442..bd6c466 100644 --- a/tests/test_frontend.py +++ b/tests/test_frontend.py @@ -27,11 +27,7 @@ def dummy_mopidy_core(): mixer = dummy_mixer.create_proxy() audio = dummy_audio.create_proxy() backend = dummy_backend.create_proxy(audio=audio) - return core.Core.start( - audio=audio, - mixer=mixer, - backends=[backend] - ).proxy() + return core.Core.start(audio=audio, mixer=mixer, backends=[backend]).proxy() def test_get_frontend_classes(): @@ -52,7 +48,9 @@ def test_frontend_handler_dispatch_play_pause(): sys.modules["RPi"] = mock.Mock() sys.modules["RPi.GPIO"] = mock.Mock() - frontend = frontend_lib.RaspberryGPIOFrontend(dummy_config, dummy_mopidy_core()) + frontend = frontend_lib.RaspberryGPIOFrontend( + dummy_config, dummy_mopidy_core() + ) frontend.dispatch_input("play_pause") @@ -61,7 +59,9 @@ def test_frontend_handler_dispatch_next(): sys.modules["RPi"] = mock.Mock() sys.modules["RPi.GPIO"] = mock.Mock() - frontend = frontend_lib.RaspberryGPIOFrontend(dummy_config, dummy_mopidy_core()) + frontend = frontend_lib.RaspberryGPIOFrontend( + dummy_config, dummy_mopidy_core() + ) frontend.dispatch_input("next") @@ -70,7 +70,9 @@ def test_frontend_handler_dispatch_prev(): sys.modules["RPi"] = mock.Mock() sys.modules["RPi.GPIO"] = mock.Mock() - frontend = frontend_lib.RaspberryGPIOFrontend(dummy_config, dummy_mopidy_core()) + frontend = frontend_lib.RaspberryGPIOFrontend( + dummy_config, dummy_mopidy_core() + ) frontend.dispatch_input("prev") @@ -79,7 +81,9 @@ def test_frontend_handler_dispatch_volume_up(): sys.modules["RPi"] = mock.Mock() sys.modules["RPi.GPIO"] = mock.Mock() - frontend = frontend_lib.RaspberryGPIOFrontend(dummy_config, dummy_mopidy_core()) + frontend = frontend_lib.RaspberryGPIOFrontend( + dummy_config, dummy_mopidy_core() + ) frontend.dispatch_input("volume_up") @@ -88,7 +92,9 @@ def test_frontend_handler_dispatch_volume_down(): sys.modules["RPi"] = mock.Mock() sys.modules["RPi.GPIO"] = mock.Mock() - frontend = frontend_lib.RaspberryGPIOFrontend(dummy_config, dummy_mopidy_core()) + frontend = frontend_lib.RaspberryGPIOFrontend( + dummy_config, dummy_mopidy_core() + ) frontend.dispatch_input("volume_down") @@ -97,7 +103,9 @@ def test_frontend_handler_dispatch_invalid_event(): sys.modules["RPi"] = mock.Mock() sys.modules["RPi.GPIO"] = mock.Mock() - frontend = frontend_lib.RaspberryGPIOFrontend(dummy_config, dummy_mopidy_core()) + frontend = frontend_lib.RaspberryGPIOFrontend( + dummy_config, dummy_mopidy_core() + ) with pytest.raises(RuntimeError): frontend.dispatch_input("tomato") @@ -107,6 +115,8 @@ def test_frontend_gpio_event(): sys.modules["RPi"] = mock.Mock() sys.modules["RPi.GPIO"] = mock.Mock() - frontend = frontend_lib.RaspberryGPIOFrontend(dummy_config, dummy_mopidy_core()) + frontend = frontend_lib.RaspberryGPIOFrontend( + dummy_config, dummy_mopidy_core() + ) frontend.gpio_event(3) diff --git a/tox.ini b/tox.ini index d92eb3d..d4deebc 100644 --- a/tox.ini +++ b/tox.ini @@ -20,4 +20,4 @@ commands = python -m check_manifest [testenv:flake8] deps = .[lint] -commands = python -m flake8 --ignore=B950 --show-source --statistics +commands = python -m flake8 --ignore=B950,B305,E501 --show-source --statistics From 6b903871e216a72944f38ae4e04ef0fa3cc28bf7 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 23 Jan 2020 13:53:49 +0000 Subject: [PATCH 18/18] Fix tests hanging --- tests/dummy_audio.py | 1 - tests/dummy_backend.py | 1 - tests/dummy_mixer.py | 1 - tests/test_frontend.py | 28 ++++++++++++++++++++++++---- 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/tests/dummy_audio.py b/tests/dummy_audio.py index 144f1a4..8e85d0a 100644 --- a/tests/dummy_audio.py +++ b/tests/dummy_audio.py @@ -6,7 +6,6 @@ tests of the core and backends. import pykka - from mopidy import audio diff --git a/tests/dummy_backend.py b/tests/dummy_backend.py index 0ee3d2d..f8e6908 100644 --- a/tests/dummy_backend.py +++ b/tests/dummy_backend.py @@ -6,7 +6,6 @@ used in tests of the frontends. import pykka - from mopidy import backend from mopidy.models import Playlist, Ref, SearchResult diff --git a/tests/dummy_mixer.py b/tests/dummy_mixer.py index 7cdffcc..b80e681 100644 --- a/tests/dummy_mixer.py +++ b/tests/dummy_mixer.py @@ -1,5 +1,4 @@ import pykka - from mopidy import mixer diff --git a/tests/test_frontend.py b/tests/test_frontend.py index bd6c466..393c69e 100644 --- a/tests/test_frontend.py +++ b/tests/test_frontend.py @@ -1,15 +1,15 @@ import sys from unittest import mock +import pykka +from mopidy import core + import pytest from mopidy_raspberry_gpio import Extension from mopidy_raspberry_gpio import frontend as frontend_lib from mopidy_raspberry_gpio import pinconfig -from mopidy import core -from . import dummy_mixer -from . import dummy_backend -from . import dummy_audio +from . import dummy_audio, dummy_backend, dummy_mixer deserialize = pinconfig.PinConfig().deserialize @@ -23,6 +23,10 @@ dummy_config = { } +def stop_mopidy_core(): + pykka.ActorRegistry.stop_all() + + def dummy_mopidy_core(): mixer = dummy_mixer.create_proxy() audio = dummy_audio.create_proxy() @@ -43,6 +47,8 @@ def test_get_frontend_classes(): "frontend", frontend_lib.RaspberryGPIOFrontend ) + stop_mopidy_core() + def test_frontend_handler_dispatch_play_pause(): sys.modules["RPi"] = mock.Mock() @@ -54,6 +60,8 @@ def test_frontend_handler_dispatch_play_pause(): frontend.dispatch_input("play_pause") + stop_mopidy_core() + def test_frontend_handler_dispatch_next(): sys.modules["RPi"] = mock.Mock() @@ -65,6 +73,8 @@ def test_frontend_handler_dispatch_next(): frontend.dispatch_input("next") + stop_mopidy_core() + def test_frontend_handler_dispatch_prev(): sys.modules["RPi"] = mock.Mock() @@ -76,6 +86,8 @@ def test_frontend_handler_dispatch_prev(): frontend.dispatch_input("prev") + stop_mopidy_core() + def test_frontend_handler_dispatch_volume_up(): sys.modules["RPi"] = mock.Mock() @@ -87,6 +99,8 @@ def test_frontend_handler_dispatch_volume_up(): frontend.dispatch_input("volume_up") + stop_mopidy_core() + def test_frontend_handler_dispatch_volume_down(): sys.modules["RPi"] = mock.Mock() @@ -98,6 +112,8 @@ def test_frontend_handler_dispatch_volume_down(): frontend.dispatch_input("volume_down") + stop_mopidy_core() + def test_frontend_handler_dispatch_invalid_event(): sys.modules["RPi"] = mock.Mock() @@ -110,6 +126,8 @@ def test_frontend_handler_dispatch_invalid_event(): with pytest.raises(RuntimeError): frontend.dispatch_input("tomato") + stop_mopidy_core() + def test_frontend_gpio_event(): sys.modules["RPi"] = mock.Mock() @@ -120,3 +138,5 @@ def test_frontend_gpio_event(): ) frontend.gpio_event(3) + + stop_mopidy_core()