From 32769f6724612a0874fe11bafb5a8f5f713f8568 Mon Sep 17 00:00:00 2001 From: Ellis Percival <flyte@failcode.co.uk> Date: Sat, 15 Jul 2017 12:28:36 +0100 Subject: [PATCH] Begin refactor/tidyup. --- .gitignore | 2 + pi_mqtt_gpio/server.py | 75 +++++++++++++++++++++++++++++++++----- tests/test_pi_mqtt_gpio.py | 2 - tests/test_server.py | 72 ++++++++++++++++++++++++++++++++++++ tox.ini | 1 + 5 files changed, 141 insertions(+), 11 deletions(-) delete mode 100644 tests/test_pi_mqtt_gpio.py create mode 100644 tests/test_server.py diff --git a/.gitignore b/.gitignore index 72364f9..3106894 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.idea + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/pi_mqtt_gpio/server.py b/pi_mqtt_gpio/server.py index b82623e..a1fa80a 100644 --- a/pi_mqtt_gpio/server.py +++ b/pi_mqtt_gpio/server.py @@ -12,12 +12,19 @@ from pi_mqtt_gpio.modules import PinPullup, PinDirection RECONNECT_DELAY_SECS = 5 GPIOS = {} LAST_STATES = {} +SET_TOPIC = "set" +OUTPUT_TOPIC = "output" + _LOG = logging.getLogger(__name__) _LOG.addHandler(logging.StreamHandler()) _LOG.setLevel(logging.DEBUG) +class CannotInstallModuleRequirements(Exception): + pass + + def on_disconnect(client, userdata, rc): _LOG.warning("Disconnected from MQTT server with code: %s" % rc) while rc != 0: @@ -26,20 +33,70 @@ def on_disconnect(client, userdata, rc): def install_missing_requirements(module): + """ + Some of the modules require external packages to be installed. This gets + the list from the `REQUIREMENTS` module attribute and attempts to + install the requirements using pip. + """ + reqs = [] try: reqs = getattr(module, "REQUIREMENTS") except AttributeError: + pass + if not reqs: _LOG.info("Module %r has no extra requirements to install." % module) return import pkg_resources - installed = pkg_resources.WorkingSet() - not_installed = [] + pkgs_installed = pkg_resources.WorkingSet() + pkgs_required = [] for req in reqs: - if installed.find(pkg_resources.Requirement.parse(req)) is None: - not_installed.append(req) - if not_installed: - import pip - pip.main(["install"] + not_installed) + if pkgs_installed.find(pkg_resources.Requirement.parse(req)) is None: + pkgs_required.append(req) + if pkgs_required: + from pip.commands.install import InstallCommand + from pip.status_codes import SUCCESS + cmd = InstallCommand() + result = cmd.main(pkgs_required) + if result != SUCCESS: + raise CannotInstallModuleRequirements( + "Unable to install packages for module %r (%s)..." % ( + module, pkgs_required)) + + +def output_name_from_topic_set(topic, topic_prefix): + """ + Return the name of the output which the topic is setting. + :param topic: str such as mytopicprefix/output/tv_lamp/set + :param topic_prefix: str prefix of our topics + :return: str name of the output this topic is setting + """ + if not topic.endswith("/%s" % SET_TOPIC): + raise ValueError("This topic does not end with '/%s'" % SET_TOPIC) + return topic[len("%s/%s/" % (topic_prefix, OUTPUT_TOPIC)):-len(SET_TOPIC)-1] + + +def init_mqtt(config): + """ + Configure MQTT client. + """ + client = mqtt.Client() + user = config["mqtt"].get("user") + password = config["mqtt"].get("password") + topic_prefix = config["mqtt"]["topic_prefix"].rstrip("/") + + if user and password: + client.username_pw_set(user, password) + + def on_conn(client, userdata, flags, rc): + for output_config in config.get("digital_outputs", []): + topic = "%s/%s/%s/%s" % (topic_prefix, OUTPUT_TOPIC, output_config["name"], SET_TOPIC) + client.subscribe(topic, qos=1) + _LOG.info("Subscribed to topic: %r", topic) + + def on_msg(client, userdata, msg): + _LOG.info("Received message on topic %r: %r", msg.topic, msg.payload) + + if __name__ == "__main__": @@ -60,7 +117,7 @@ if __name__ == "__main__": def on_conn(client, userdata, flags, rc): for output_config in config.get("digital_outputs", []): - topic = "%s/output/%s/set" % (topic_prefix, output_config["name"]) + topic = "%s/output/%s/%s" % (topic_prefix, output_config["name"], SET_TOPIC) client.subscribe(topic, qos=1) _LOG.info("Subscribed to topic: %r", topic) @@ -138,7 +195,7 @@ if __name__ == "__main__": LAST_STATES[input_config["name"]] = state sleep(0.05) except KeyboardInterrupt: - print "" + print("") finally: client.disconnect() client.loop_stop() diff --git a/tests/test_pi_mqtt_gpio.py b/tests/test_pi_mqtt_gpio.py deleted file mode 100644 index 61de3a2..0000000 --- a/tests/test_pi_mqtt_gpio.py +++ /dev/null @@ -1,2 +0,0 @@ -def test_noop(): - pass diff --git a/tests/test_server.py b/tests/test_server.py new file mode 100644 index 0000000..d8c96c2 --- /dev/null +++ b/tests/test_server.py @@ -0,0 +1,72 @@ +import uuid + +import mock +import pytest +from pip.status_codes import SUCCESS + +from pi_mqtt_gpio import server + + +@mock.patch("pkg_resources.WorkingSet") +def test_imr_no_attribute(mock_ws): + """ + Should not bother looking up what's installed when there's no requirements. + """ + module = object() + server.install_missing_requirements(module) + mock_ws.assert_not_called() + + +@mock.patch("pkg_resources.WorkingSet") +def test_imr_blank_list(mock_ws): + """ + Should not bother looking up what's installed when there's no requirements. + """ + module = mock.Mock() + module.REQUIREMENTS = [] + server.install_missing_requirements(module) + mock_ws.assert_not_called() + + +@mock.patch("pkg_resources.WorkingSet.find", return_value=None) +@mock.patch("pip.commands.install.InstallCommand.main", return_value=SUCCESS) +def test_imr_has_requirements(mock_pip, mock_find): + """ + Should install all missing args. + """ + module = mock.Mock() + module.REQUIREMENTS = ["testreq1", "testreq2==1.2.3"] + server.install_missing_requirements(module) + args, _ = mock_pip.call_args + assert args == (["testreq1", "testreq2==1.2.3"],) + + +@mock.patch("pkg_resources.WorkingSet.find", return_value=None) +def test_imr_bad_pkg_fails(mock_find): + """ + Should raise exception when pkg installation fails. + """ + module = mock.Mock() + module.REQUIREMENTS = [str(uuid.uuid4())] + with pytest.raises(server.CannotInstallModuleRequirements): + server.install_missing_requirements(module) + + +def test_onfts_no_set(): + """ + Should raise a ValueError when there's no /set at the end of the topic. + """ + with pytest.raises(ValueError): + server.output_name_from_topic_set("myprefix/output/myoutputname", "myprefix") + + +def test_onfts_returns_output_name(): + """ + Should return the proper output name. + """ + output_name = "myoutputname" + ret = server.output_name_from_topic_set( + "myprefix/output/%s/%s" % (output_name, server.SET_TOPIC), + "myprefix" + ) + assert ret == output_name diff --git a/tox.ini b/tox.ini index 26b89ad..fdd9478 100644 --- a/tox.ini +++ b/tox.ini @@ -6,6 +6,7 @@ deps = -rrequirements.txt flake8 pytest + mock setenv = PYTHONPATH = {toxinidir} commands = -- GitLab