From 85743d679b71184569c8a8befa2b098f103957f6 Mon Sep 17 00:00:00 2001 From: Ely Deckers Date: Sun, 13 Mar 2022 18:16:07 +0100 Subject: [PATCH 1/7] refactor: inject processor method into commands --- .pre-commit-config.yaml | 1 + src/huemon/__main__.py | 3 +- src/huemon/api_server.py | 3 +- src/huemon/commands/command_handler.py | 25 ++++--- src/huemon/commands/hue_command_interface.py | 14 ++-- src/huemon/commands/internal/agent_command.py | 8 +-- .../commands/internal/discover_command.py | 7 +- src/huemon/commands/internal/light_command.py | 8 +-- .../commands/internal/sensor_command.py | 8 +-- .../commands/internal/system_command.py | 8 +-- src/huemon/processors/__init__.py | 4 ++ src/huemon/processors/processor_interface.py | 11 +++ src/huemon/processors/stdout_processor.py | 12 ++++ src/mypy.ini | 1 + src/tests/test_command_handler.py | 30 ++++---- src/tests/test_light_command.py | 70 +++++++------------ src/tests/test_sensor_command.py | 65 +++++++---------- src/tests/test_system_command.py | 32 ++++----- 18 files changed, 145 insertions(+), 165 deletions(-) create mode 100644 src/huemon/processors/__init__.py create mode 100644 src/huemon/processors/processor_interface.py create mode 100644 src/huemon/processors/stdout_processor.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0655bdb..c5feca8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,3 +27,4 @@ repos: rev: v0.940 hooks: - id: mypy + additional_dependencies: [types-PyYAML] diff --git a/src/huemon/__main__.py b/src/huemon/__main__.py index 2aebe8b..388080d 100755 --- a/src/huemon/__main__.py +++ b/src/huemon/__main__.py @@ -9,6 +9,7 @@ from huemon.infrastructure.bootstrapper import bootstrap from huemon.infrastructure.config_factory import create_config from huemon.infrastructure.logger_factory import bootstrap_logger +from huemon.processors.stdout_processor import StdoutProcessor from huemon.utils.const import EXIT_OK from huemon.utils.errors import exit_fail from huemon.utils.plugins import get_command_plugins_path @@ -24,7 +25,7 @@ def main(argv): bootstrap() command_handler = create_default_command_handler( - CONFIG, get_command_plugins_path(CONFIG) + CONFIG, StdoutProcessor(), get_command_plugins_path(CONFIG) ) if len(argv) <= 1: diff --git a/src/huemon/api_server.py b/src/huemon/api_server.py index d020b32..cc5e174 100644 --- a/src/huemon/api_server.py +++ b/src/huemon/api_server.py @@ -12,6 +12,7 @@ from huemon.commands.command_handler import create_default_command_handler from huemon.infrastructure.logger_factory import create_logger +from huemon.processors.stdout_processor import StdoutProcessor from huemon.utils.plugins import get_command_plugins_path LOG = create_logger() @@ -47,7 +48,7 @@ def create(config: dict) -> FastAPI: app = FastAPI() command_handler = create_default_command_handler( - config, get_command_plugins_path(config) + config, StdoutProcessor(), get_command_plugins_path(config) ) for command_name in command_handler.available_commands(): diff --git a/src/huemon/commands/command_handler.py b/src/huemon/commands/command_handler.py index 06ae9fa..6bc0b8e 100644 --- a/src/huemon/commands/command_handler.py +++ b/src/huemon/commands/command_handler.py @@ -12,6 +12,7 @@ from huemon.commands.hue_command_interface import HueCommand from huemon.infrastructure.logger_factory import create_logger from huemon.infrastructure.plugin_loader import load_plugins +from huemon.processors.processor_interface import ProcessorInterface from huemon.utils.errors import exit_fail from huemon.utils.monads.either import rights from huemon.utils.paths import create_local_path @@ -20,12 +21,17 @@ def create_name_to_command_mapping( - config: dict, api: ApiInterface, plugins: List[Type[HueCommand]] + config: dict, + api: ApiInterface, + processor: ProcessorInterface, + plugins: List[Type[HueCommand]], ) -> dict: - return reduce(lambda p, c: {**p, c.name(): c(config, api)}, plugins, {}) + return reduce(lambda p, c: {**p, c.name(): c(config, api, processor)}, plugins, {}) -def __load_command_plugins(config: dict, command_plugins_path: str = None) -> dict: +def __load_command_plugins( + config: dict, processor: ProcessorInterface, command_plugins_path: str = None +) -> dict: LOG.debug("Loading command plugins (path=%s)", command_plugins_path) if not command_plugins_path: return {} @@ -35,6 +41,7 @@ def __load_command_plugins(config: dict, command_plugins_path: str = None) -> di command_handler_plugins = create_name_to_command_mapping( config, create_api(config), + processor, command_plugins, ) LOG.debug("Finished loading command plugins (path=%s)", command_plugins_path) @@ -43,19 +50,21 @@ def __load_command_plugins(config: dict, command_plugins_path: str = None) -> di def __load_plugins_and_hardwired_handlers( - config: dict, command_plugins_path: str = None + config: dict, processor: ProcessorInterface, command_plugins_path: str = None ) -> dict: hardwired_commands_path = create_local_path(os.path.join("commands", "internal")) return { - **__load_command_plugins(config, command_plugins_path), - **__load_command_plugins(config, hardwired_commands_path), + **__load_command_plugins(config, processor, command_plugins_path), + **__load_command_plugins(config, processor, hardwired_commands_path), } -def create_default_command_handler(config: dict, command_plugins_path: str): +def create_default_command_handler( + config: dict, processor: ProcessorInterface, command_plugins_path: str = None +): return CommandHandler( - __load_plugins_and_hardwired_handlers(config, command_plugins_path) + __load_plugins_and_hardwired_handlers(config, processor, command_plugins_path) ) diff --git a/src/huemon/commands/hue_command_interface.py b/src/huemon/commands/hue_command_interface.py index bef84b5..de20251 100644 --- a/src/huemon/commands/hue_command_interface.py +++ b/src/huemon/commands/hue_command_interface.py @@ -6,11 +6,14 @@ from functools import reduce from huemon.api.api_interface import ApiInterface +from huemon.processors.processor_interface import ProcessorInterface class HueCommand: - def __init__(self, config: dict, api: ApiInterface): - raise NotImplementedError("Command requires a constructor") + def __init__(self, config: dict, api: ApiInterface, processor: ProcessorInterface): + self.config = config + self.api = api + self.processor = processor @staticmethod def get_by_unique_id(unique_id: str, items: list) -> list: @@ -20,10 +23,6 @@ def get_by_unique_id(unique_id: str, items: list) -> list: ) )[0] - @staticmethod - def _process(value): - print(value) - @staticmethod def _mapper(path: str, value_type): return lambda value: value_type( @@ -34,5 +33,8 @@ def _mapper(path: str, value_type): def name(): pass + def _process(self, value): + self.processor.process(value) + def exec(self, arguments): pass diff --git a/src/huemon/commands/internal/agent_command.py b/src/huemon/commands/internal/agent_command.py index bb1d82e..de492a7 100644 --- a/src/huemon/commands/internal/agent_command.py +++ b/src/huemon/commands/internal/agent_command.py @@ -5,7 +5,6 @@ import uvicorn # type: ignore -from huemon.api.api_interface import ApiInterface from huemon.api_server import HuemonServerFactory from huemon.commands.hue_command_interface import HueCommand from huemon.infrastructure.logger_factory import create_logger @@ -38,11 +37,6 @@ class AgentCommand(HueCommand): "start": MyServer.start, } - def __init__( - self, config: dict, _: ApiInterface - ): # pylint: disable=unused-argument - self.config = config - @staticmethod def name(): return "agent" @@ -55,7 +49,7 @@ def exec(self, arguments): assert_exists(list(AgentCommand.__SYSTEM_ACTION_MAP), action) - HueCommand._process(self.__SYSTEM_ACTION_MAP[action](self.config)) + self._process(self.__SYSTEM_ACTION_MAP[action](self.config)) LOG.debug( "Finished `%s` command (arguments=%s)", AgentCommand.name(), arguments ) diff --git a/src/huemon/commands/internal/discover_command.py b/src/huemon/commands/internal/discover_command.py index 7ed4b73..906ee56 100644 --- a/src/huemon/commands/internal/discover_command.py +++ b/src/huemon/commands/internal/discover_command.py @@ -12,6 +12,7 @@ from huemon.discoveries.discovery_interface import Discovery from huemon.infrastructure.logger_factory import create_logger from huemon.infrastructure.plugin_loader import load_plugins +from huemon.processors.processor_interface import ProcessorInterface from huemon.utils.assertions import assert_exists, assert_num_args from huemon.utils.monads.either import Either, rights from huemon.utils.monads.maybe import Maybe, maybe, of @@ -95,9 +96,9 @@ def discover(self, discovery_type): class DiscoverCommand(HueCommand): - def __init__( - self, config: dict, api: ApiInterface - ): # pylint: disable=super-init-not-called + def __init__(self, config: dict, api: ApiInterface, processor: ProcessorInterface): + super().__init__(config, api, processor) + self.discovery = Discover(config, api) @staticmethod diff --git a/src/huemon/commands/internal/light_command.py b/src/huemon/commands/internal/light_command.py index afa103e..ac5dd42 100644 --- a/src/huemon/commands/internal/light_command.py +++ b/src/huemon/commands/internal/light_command.py @@ -3,7 +3,6 @@ # This source code is licensed under the MPL-2.0 license found in the # LICENSE file in the root directory of this source tree. -from huemon.api.api_interface import ApiInterface from huemon.commands.hue_command_interface import HueCommand from huemon.infrastructure.logger_factory import create_logger from huemon.utils.assertions import assert_exists, assert_num_args @@ -12,11 +11,6 @@ class LightCommand(HueCommand): - def __init__( - self, config: dict, api: ApiInterface - ): # pylint: disable=unused-argument - self.api = api - __LIGHT_ACTION_MAP = { "is_upgrade_available": lambda light: int( light["swupdate"]["state"] != "noupdates" @@ -44,7 +38,7 @@ def exec(self, arguments): assert_exists(list(LightCommand.__LIGHT_ACTION_MAP), action) - HueCommand._process( + self._process( self.__map_light(light_id, LightCommand.__LIGHT_ACTION_MAP[action]) ) diff --git a/src/huemon/commands/internal/sensor_command.py b/src/huemon/commands/internal/sensor_command.py index e52ceb3..1a32569 100644 --- a/src/huemon/commands/internal/sensor_command.py +++ b/src/huemon/commands/internal/sensor_command.py @@ -3,7 +3,6 @@ # This source code is licensed under the MPL-2.0 license found in the # LICENSE file in the root directory of this source tree. -from huemon.api.api_interface import ApiInterface from huemon.commands.hue_command_interface import HueCommand from huemon.infrastructure.logger_factory import create_logger from huemon.utils.assertions import assert_exists, assert_num_args @@ -12,11 +11,6 @@ class SensorCommand(HueCommand): - def __init__( - self, config: dict, api: ApiInterface - ): # pylint: disable=unused-argument - self.api = api - def __get_sensor(self, device_id): return HueCommand.get_by_unique_id(device_id, self.api.get_sensors()) @@ -45,7 +39,7 @@ def exec(self, arguments): assert_exists(list(SensorCommand.__SENSOR_ACTION_MAP), action) - HueCommand._process( + self._process( self.__map_sensor(device_id, SensorCommand.__SENSOR_ACTION_MAP[action]) ) LOG.debug( diff --git a/src/huemon/commands/internal/system_command.py b/src/huemon/commands/internal/system_command.py index 5fccd5b..692010b 100644 --- a/src/huemon/commands/internal/system_command.py +++ b/src/huemon/commands/internal/system_command.py @@ -3,7 +3,6 @@ # This source code is licensed under the MPL-2.0 license found in the # LICENSE file in the root directory of this source tree. -from huemon.api.api_interface import ApiInterface from huemon.commands.hue_command_interface import HueCommand from huemon.infrastructure.logger_factory import create_logger from huemon.utils.assertions import assert_exists, assert_num_args @@ -12,11 +11,6 @@ class SystemCommand(HueCommand): - def __init__( - self, config: dict, api: ApiInterface - ): # pylint: disable=unused-argument - self.api = api - def __map_config(self, mapper): return mapper(self.api.get_system_config()) @@ -41,7 +35,7 @@ def exec(self, arguments): assert_exists(list(SystemCommand.__SYSTEM_ACTION_MAP), action) - HueCommand._process(self.__map_config(self.__SYSTEM_ACTION_MAP[action])) + self._process(self.__map_config(self.__SYSTEM_ACTION_MAP[action])) LOG.debug( "Finished `%s` command (arguments=%s)", SystemCommand.name(), arguments ) diff --git a/src/huemon/processors/__init__.py b/src/huemon/processors/__init__.py new file mode 100644 index 0000000..e42426a --- /dev/null +++ b/src/huemon/processors/__init__.py @@ -0,0 +1,4 @@ +# Copyright (c) Ely Deckers. +# +# This source code is licensed under the MPL-2.0 license found in the +# LICENSE file in the root directory of this source tree. diff --git a/src/huemon/processors/processor_interface.py b/src/huemon/processors/processor_interface.py new file mode 100644 index 0000000..22e0ca5 --- /dev/null +++ b/src/huemon/processors/processor_interface.py @@ -0,0 +1,11 @@ +from typing import Generic, TypeVar + +from huemon.utils.errors import HueError +from huemon.utils.monads.either import Either + +TA = TypeVar("TA") + + +class ProcessorInterface(Generic[TA]): # pylint: disable=too-few-public-methods + def process(self, value: Either[HueError, TA]): + raise NotImplementedError("Processor process implementation missing") diff --git a/src/huemon/processors/stdout_processor.py b/src/huemon/processors/stdout_processor.py new file mode 100644 index 0000000..06831aa --- /dev/null +++ b/src/huemon/processors/stdout_processor.py @@ -0,0 +1,12 @@ +from typing import TypeVar + +from huemon.processors.processor_interface import ProcessorInterface +from huemon.utils.errors import HueError +from huemon.utils.monads.either import Either + +TA = TypeVar("TA") + + +class StdoutProcessor(ProcessorInterface): # pylint: disable=too-few-public-methods + def process(self, value: Either[HueError, TA]): + print(value) diff --git a/src/mypy.ini b/src/mypy.ini index 47d58fc..af66412 100644 --- a/src/mypy.ini +++ b/src/mypy.ini @@ -1,3 +1,4 @@ [mypy] check_untyped_defs = true strict_optional = true +ignore_missing_imports = true diff --git a/src/tests/test_command_handler.py b/src/tests/test_command_handler.py index 82c23c4..3e967c5 100644 --- a/src/tests/test_command_handler.py +++ b/src/tests/test_command_handler.py @@ -11,17 +11,27 @@ create_name_to_command_mapping, ) from huemon.commands.internal.system_command import SystemCommand +from huemon.processors.stdout_processor import StdoutProcessor from huemon.utils.const import EXIT_FAIL from tests.fixtures import MutableApi, create_system_config +def _ch(system_config: dict): + mutable_api = MutableApi() + mutable_api.set_system_config(system_config) + + return CommandHandler( + create_name_to_command_mapping( + {}, mutable_api, StdoutProcessor(), [SystemCommand] + ) + ) + + class TestCachedApi(unittest.TestCase): def test_when_command_is_loaded_it_should_be_listed_as_available(self): vanilla_command_handler = CommandHandler([]) - command_handler = CommandHandler( - create_name_to_command_mapping({}, MutableApi(), [SystemCommand]) - ) + command_handler = _ch({}) expected_command = SystemCommand.name() @@ -41,25 +51,17 @@ def test_when_command_is_loaded_it_should_be_listed_as_available(self): def test_when_cache_not_expired_return_cache(mock_print: MagicMock): some_version = "TEST_VERSION" - mutable_api = MutableApi() - - system_config_pre = create_system_config(version=some_version) - - mutable_api.set_system_config(system_config_pre) - - command_handler = CommandHandler( - create_name_to_command_mapping({}, mutable_api, [SystemCommand]) - ) + command_handler = _ch(create_system_config(version=some_version)) command_handler.exec("system", ["version"]) mock_print.assert_called_once_with(some_version) def test_when_unknown_command_received_system_exit_is_called(self): - command_handler = CommandHandler([]) + vanilla_command_handler = CommandHandler([]) with self.assertRaises(SystemExit) as failed_call_context: - command_handler.exec("system", ["version"]) + vanilla_command_handler.exec("system", ["version"]) self.assertEqual( EXIT_FAIL, diff --git a/src/tests/test_light_command.py b/src/tests/test_light_command.py index 1c47ac5..0a584c1 100644 --- a/src/tests/test_light_command.py +++ b/src/tests/test_light_command.py @@ -4,6 +4,7 @@ # LICENSE file in the root directory of this source tree. import unittest +from typing import List from unittest.mock import MagicMock, call, patch from huemon.commands.command_handler import ( @@ -11,6 +12,7 @@ create_name_to_command_mapping, ) from huemon.commands.internal.light_command import LightCommand +from huemon.processors.stdout_processor import StdoutProcessor from huemon.utils.const import EXIT_FAIL from tests.fixtures import MutableApi @@ -18,25 +20,26 @@ SOME_LIGHT_MAC_1 = "SO:ME:LI:GH:TM:AC:01" -class TestLightCommand(unittest.TestCase): - def test_when_light_doesnt_exist_raise(self): - mutable_api = MutableApi() - mutable_api.set_lights([]) +def _ch(lights: List[dict]): + mutable_api = MutableApi() + mutable_api.set_lights(lights) - command_handler = CommandHandler( - create_name_to_command_mapping({}, mutable_api, [LightCommand]) + return CommandHandler( + create_name_to_command_mapping( + {}, mutable_api, StdoutProcessor(), [LightCommand] ) + ) + + +class TestLightCommand(unittest.TestCase): + def test_when_light_doesnt_exist_raise(self): + command_handler = _ch([]) with self.assertRaises(Exception): command_handler.exec("light", [SOME_LIGHT_MAC_0, "status"]) def test_when_not_enough_parameters_raise(self): - mutable_api = MutableApi() - mutable_api.set_lights([]) - - command_handler = CommandHandler( - create_name_to_command_mapping({}, mutable_api, [LightCommand]) - ) + command_handler = _ch([]) with self.assertRaises(SystemExit) as failed_call_context: command_handler.exec("light", []) @@ -48,12 +51,7 @@ def test_when_not_enough_parameters_raise(self): ) def test_when_unknown_action_raise(self): - mutable_api = MutableApi() - mutable_api.set_lights([]) - - command_handler = CommandHandler( - create_name_to_command_mapping({}, mutable_api, [LightCommand]) - ) + command_handler = _ch([]) with self.assertRaises(SystemExit) as failed_call_context: command_handler.exec("light", [SOME_LIGHT_MAC_0, "some_unknown_action"]) @@ -67,8 +65,7 @@ def test_when_unknown_action_raise(self): @staticmethod @patch("builtins.print") def test_when_light_exists_return_status(mock_print: MagicMock): - mutable_api = MutableApi() - mutable_api.set_lights( + command_handler = _ch( [ { "uniqueid": SOME_LIGHT_MAC_0, @@ -85,20 +82,15 @@ def test_when_light_exists_return_status(mock_print: MagicMock): ] ) - command_handler = CommandHandler( - create_name_to_command_mapping({}, mutable_api, [LightCommand]) - ) - command_handler.exec("light", [SOME_LIGHT_MAC_0, "status"]) command_handler.exec("light", [SOME_LIGHT_MAC_1, "status"]) - mock_print.assert_has_calls(map(call, [1, 0])) + mock_print.assert_has_calls(list(map(call, [1, 0]))) @staticmethod @patch("builtins.print") def test_when_light_exists_return_is_upgrade_available(mock_print: MagicMock): - mutable_api = MutableApi() - mutable_api.set_lights( + command_handler = _ch( [ { "uniqueid": SOME_LIGHT_MAC_0, @@ -115,20 +107,15 @@ def test_when_light_exists_return_is_upgrade_available(mock_print: MagicMock): ] ) - command_handler = CommandHandler( - create_name_to_command_mapping({}, mutable_api, [LightCommand]) - ) - command_handler.exec("light", [SOME_LIGHT_MAC_0, "is_upgrade_available"]) command_handler.exec("light", [SOME_LIGHT_MAC_1, "is_upgrade_available"]) - mock_print.assert_has_calls(map(call, [0, 1])) + mock_print.assert_has_calls(list(map(call, [0, 1]))) @staticmethod @patch("builtins.print") def test_when_light_exists_return_is_reachable(mock_print: MagicMock): - mutable_api = MutableApi() - mutable_api.set_lights( + command_handler = _ch( [ {"uniqueid": SOME_LIGHT_MAC_0, "state": {"reachable": 0}}, { @@ -140,14 +127,10 @@ def test_when_light_exists_return_is_reachable(mock_print: MagicMock): ] ) - command_handler = CommandHandler( - create_name_to_command_mapping({}, mutable_api, [LightCommand]) - ) - command_handler.exec("light", [SOME_LIGHT_MAC_0, "reachable"]) command_handler.exec("light", [SOME_LIGHT_MAC_1, "reachable"]) - mock_print.assert_has_calls(map(call, [0, 1])) + mock_print.assert_has_calls(list(map(call, [0, 1]))) @staticmethod @patch("builtins.print") @@ -155,8 +138,7 @@ def test_when_light_exists_return_version(mock_print: MagicMock): some_version_0 = "some_version_0" some_version_1 = "some_version_1" - mutable_api = MutableApi() - mutable_api.set_lights( + command_handler = _ch( [ { "uniqueid": SOME_LIGHT_MAC_0, @@ -169,11 +151,7 @@ def test_when_light_exists_return_version(mock_print: MagicMock): ] ) - command_handler = CommandHandler( - create_name_to_command_mapping({}, mutable_api, [LightCommand]) - ) - command_handler.exec("light", [SOME_LIGHT_MAC_0, "version"]) command_handler.exec("light", [SOME_LIGHT_MAC_1, "version"]) - mock_print.assert_has_calls(map(call, [some_version_0, some_version_1])) + mock_print.assert_has_calls(list(map(call, [some_version_0, some_version_1]))) diff --git a/src/tests/test_sensor_command.py b/src/tests/test_sensor_command.py index fdd20b0..e65c4f7 100644 --- a/src/tests/test_sensor_command.py +++ b/src/tests/test_sensor_command.py @@ -4,6 +4,7 @@ # LICENSE file in the root directory of this source tree. import unittest +from typing import List from unittest.mock import MagicMock, call, patch from huemon.commands.command_handler import ( @@ -11,20 +12,27 @@ create_name_to_command_mapping, ) from huemon.commands.internal.sensor_command import SensorCommand +from huemon.processors.stdout_processor import StdoutProcessor from tests.fixtures import MutableApi SOME_SENSOR_MAC_0 = "SO:ME:SE:NS:OR:MA:C0" SOME_SENSOR_MAC_1 = "SO:ME:SE:NS:OR:MA:C1" -class TestSensorCommand(unittest.TestCase): - def test_when_sensor_doesnt_exist(self): - mutable_api = MutableApi() - mutable_api.set_sensors([]) +def _ch(sensors: List[dict]): + mutable_api = MutableApi() + mutable_api.set_sensors(sensors) - command_handler = CommandHandler( - create_name_to_command_mapping({}, mutable_api, [SensorCommand]) + return CommandHandler( + create_name_to_command_mapping( + {}, mutable_api, StdoutProcessor(), [SensorCommand] ) + ) + + +class TestSensorCommand(unittest.TestCase): + def test_when_sensor_doesnt_exist(self): + command_handler = _ch([]) with self.assertRaises(Exception): command_handler.exec("sensor", [SOME_SENSOR_MAC_0, "battery:level"]) @@ -35,8 +43,7 @@ def test_when_sensor_exists_return_battery_level(mock_print: MagicMock): some_level_0 = 80 some_level_1 = 1 - mutable_api = MutableApi() - mutable_api.set_sensors( + command_handler = _ch( [ { "uniqueid": SOME_SENSOR_MAC_0, @@ -53,14 +60,10 @@ def test_when_sensor_exists_return_battery_level(mock_print: MagicMock): ] ) - command_handler = CommandHandler( - create_name_to_command_mapping({}, mutable_api, [SensorCommand]) - ) - command_handler.exec("sensor", [SOME_SENSOR_MAC_0, "battery:level"]) command_handler.exec("sensor", [SOME_SENSOR_MAC_1, "battery:level"]) - mock_print.assert_has_calls(map(call, [some_level_0, some_level_1])) + mock_print.assert_has_calls(list(map(call, [some_level_0, some_level_1]))) @staticmethod @patch("builtins.print") @@ -68,8 +71,7 @@ def test_when_sensor_exists_return_light_level(mock_print: MagicMock): some_level_0 = 80 some_level_1 = 1 - mutable_api = MutableApi() - mutable_api.set_sensors( + command_handler = _ch( [ { "uniqueid": SOME_SENSOR_MAC_0, @@ -86,14 +88,10 @@ def test_when_sensor_exists_return_light_level(mock_print: MagicMock): ] ) - command_handler = CommandHandler( - create_name_to_command_mapping({}, mutable_api, [SensorCommand]) - ) - command_handler.exec("sensor", [SOME_SENSOR_MAC_0, "light:level"]) command_handler.exec("sensor", [SOME_SENSOR_MAC_1, "light:level"]) - mock_print.assert_has_calls(map(call, [some_level_0, some_level_1])) + mock_print.assert_has_calls(list(map(call, [some_level_0, some_level_1]))) @staticmethod @patch("builtins.print") @@ -101,8 +99,7 @@ def test_when_sensor_exists_return_temperature(mock_print: MagicMock): some_temperature_0 = 80 some_temperature_1 = 1 - mutable_api = MutableApi() - mutable_api.set_sensors( + command_handler = _ch( [ { "uniqueid": SOME_SENSOR_MAC_0, @@ -119,15 +116,11 @@ def test_when_sensor_exists_return_temperature(mock_print: MagicMock): ] ) - command_handler = CommandHandler( - create_name_to_command_mapping({}, mutable_api, [SensorCommand]) - ) - command_handler.exec("sensor", [SOME_SENSOR_MAC_0, "temperature"]) command_handler.exec("sensor", [SOME_SENSOR_MAC_1, "temperature"]) mock_print.assert_has_calls( - map(call, [some_temperature_0 * 0.01, some_temperature_1 * 0.01]) + list(map(call, [some_temperature_0 * 0.01, some_temperature_1 * 0.01])) ) @staticmethod @@ -136,8 +129,7 @@ def test_when_sensor_exists_return_presence(mock_print: MagicMock): some_presence_0 = 80 some_presence_1 = 1 - mutable_api = MutableApi() - mutable_api.set_sensors( + command_handler = _ch( [ { "uniqueid": SOME_SENSOR_MAC_0, @@ -154,14 +146,10 @@ def test_when_sensor_exists_return_presence(mock_print: MagicMock): ] ) - command_handler = CommandHandler( - create_name_to_command_mapping({}, mutable_api, [SensorCommand]) - ) - command_handler.exec("sensor", [SOME_SENSOR_MAC_0, "presence"]) command_handler.exec("sensor", [SOME_SENSOR_MAC_1, "presence"]) - mock_print.assert_has_calls(map(call, [some_presence_0, some_presence_1])) + mock_print.assert_has_calls(list(map(call, [some_presence_0, some_presence_1]))) @staticmethod @patch("builtins.print") @@ -169,8 +157,7 @@ def test_when_sensor_exists_return_reachable(mock_print: MagicMock): some_reachability_0 = 0 some_reachability_1 = 1 - mutable_api = MutableApi() - mutable_api.set_sensors( + command_handler = _ch( [ { "uniqueid": SOME_SENSOR_MAC_0, @@ -187,13 +174,9 @@ def test_when_sensor_exists_return_reachable(mock_print: MagicMock): ] ) - command_handler = CommandHandler( - create_name_to_command_mapping({}, mutable_api, [SensorCommand]) - ) - command_handler.exec("sensor", [SOME_SENSOR_MAC_0, "reachable"]) command_handler.exec("sensor", [SOME_SENSOR_MAC_1, "reachable"]) mock_print.assert_has_calls( - map(call, [some_reachability_0, some_reachability_1]) + list(map(call, [some_reachability_0, some_reachability_1])) ) diff --git a/src/tests/test_system_command.py b/src/tests/test_system_command.py index 528139f..049aee2 100644 --- a/src/tests/test_system_command.py +++ b/src/tests/test_system_command.py @@ -11,26 +11,33 @@ create_name_to_command_mapping, ) from huemon.commands.internal.system_command import SystemCommand +from huemon.processors.stdout_processor import StdoutProcessor from tests.fixtures import MutableApi +def _ch(system_config: dict): + mutable_api = MutableApi() + mutable_api.set_system_config(system_config) + + return CommandHandler( + create_name_to_command_mapping( + {}, mutable_api, StdoutProcessor(), [SystemCommand] + ) + ) + + class TestCachedApi(unittest.TestCase): @staticmethod @patch("builtins.print") def test_when_system_version_received_print(mock_print: MagicMock): some_version = "SOME_VERSION_0" - mutable_api = MutableApi() - mutable_api.set_system_config( + command_handler = _ch( { "swversion": some_version, } ) - command_handler = CommandHandler( - create_name_to_command_mapping({}, mutable_api, [SystemCommand]) - ) - command_handler.exec("system", ["version"]) mock_print.assert_called_once_with(some_version) @@ -38,8 +45,7 @@ def test_when_system_version_received_print(mock_print: MagicMock): @staticmethod @patch("builtins.print") def test_when_system_upgrade_available_print(mock_print: MagicMock): - mutable_api_0 = MutableApi() - mutable_api_0.set_system_config( + command_handler_0 = _ch( { "swupdate2": { "bridge": { @@ -49,8 +55,7 @@ def test_when_system_upgrade_available_print(mock_print: MagicMock): } ) - mutable_api_1 = MutableApi() - mutable_api_1.set_system_config( + command_handler_1 = _ch( { "swupdate2": { "bridge": { @@ -60,13 +65,6 @@ def test_when_system_upgrade_available_print(mock_print: MagicMock): } ) - command_handler_0 = CommandHandler( - create_name_to_command_mapping({}, mutable_api_0, [SystemCommand]) - ) - command_handler_1 = CommandHandler( - create_name_to_command_mapping({}, mutable_api_1, [SystemCommand]) - ) - command_handler_0.exec("system", ["is_upgrade_available"]) command_handler_1.exec("system", ["is_upgrade_available"]) From d15943f571e0a368a224a2dff4f6fe5c9a116e61 Mon Sep 17 00:00:00 2001 From: Ely Deckers Date: Sun, 13 Mar 2022 18:22:38 +0100 Subject: [PATCH 2/7] refactor: rename Processor to Sink --- src/huemon/__main__.py | 4 ++-- src/huemon/api_server.py | 4 ++-- src/huemon/commands/command_handler.py | 10 +++++----- src/huemon/commands/hue_command_interface.py | 4 ++-- .../commands/internal/discover_command.py | 4 ++-- src/huemon/processors/stdout_processor.py | 12 ------------ src/huemon/{processors => sinks}/__init__.py | 0 .../sink_interface.py} | 7 ++++++- src/huemon/sinks/stdout_sink.py | 17 +++++++++++++++++ src/tests/test_command_handler.py | 6 ++---- src/tests/test_light_command.py | 6 ++---- src/tests/test_sensor_command.py | 6 ++---- src/tests/test_system_command.py | 6 ++---- 13 files changed, 44 insertions(+), 42 deletions(-) delete mode 100644 src/huemon/processors/stdout_processor.py rename src/huemon/{processors => sinks}/__init__.py (100%) rename src/huemon/{processors/processor_interface.py => sinks/sink_interface.py} (53%) create mode 100644 src/huemon/sinks/stdout_sink.py diff --git a/src/huemon/__main__.py b/src/huemon/__main__.py index 388080d..c75ff48 100755 --- a/src/huemon/__main__.py +++ b/src/huemon/__main__.py @@ -9,7 +9,7 @@ from huemon.infrastructure.bootstrapper import bootstrap from huemon.infrastructure.config_factory import create_config from huemon.infrastructure.logger_factory import bootstrap_logger -from huemon.processors.stdout_processor import StdoutProcessor +from huemon.sinks.stdout_sink import StdoutSink from huemon.utils.const import EXIT_OK from huemon.utils.errors import exit_fail from huemon.utils.plugins import get_command_plugins_path @@ -25,7 +25,7 @@ def main(argv): bootstrap() command_handler = create_default_command_handler( - CONFIG, StdoutProcessor(), get_command_plugins_path(CONFIG) + CONFIG, StdoutSink(), get_command_plugins_path(CONFIG) ) if len(argv) <= 1: diff --git a/src/huemon/api_server.py b/src/huemon/api_server.py index cc5e174..94a5b5b 100644 --- a/src/huemon/api_server.py +++ b/src/huemon/api_server.py @@ -12,7 +12,7 @@ from huemon.commands.command_handler import create_default_command_handler from huemon.infrastructure.logger_factory import create_logger -from huemon.processors.stdout_processor import StdoutProcessor +from huemon.sinks.stdout_sink import StdoutSink from huemon.utils.plugins import get_command_plugins_path LOG = create_logger() @@ -48,7 +48,7 @@ def create(config: dict) -> FastAPI: app = FastAPI() command_handler = create_default_command_handler( - config, StdoutProcessor(), get_command_plugins_path(config) + config, StdoutSink(), get_command_plugins_path(config) ) for command_name in command_handler.available_commands(): diff --git a/src/huemon/commands/command_handler.py b/src/huemon/commands/command_handler.py index 6bc0b8e..13f7b36 100644 --- a/src/huemon/commands/command_handler.py +++ b/src/huemon/commands/command_handler.py @@ -12,7 +12,7 @@ from huemon.commands.hue_command_interface import HueCommand from huemon.infrastructure.logger_factory import create_logger from huemon.infrastructure.plugin_loader import load_plugins -from huemon.processors.processor_interface import ProcessorInterface +from huemon.sinks.sink_interface import SinkInterface from huemon.utils.errors import exit_fail from huemon.utils.monads.either import rights from huemon.utils.paths import create_local_path @@ -23,14 +23,14 @@ def create_name_to_command_mapping( config: dict, api: ApiInterface, - processor: ProcessorInterface, + processor: SinkInterface, plugins: List[Type[HueCommand]], ) -> dict: return reduce(lambda p, c: {**p, c.name(): c(config, api, processor)}, plugins, {}) def __load_command_plugins( - config: dict, processor: ProcessorInterface, command_plugins_path: str = None + config: dict, processor: SinkInterface, command_plugins_path: str = None ) -> dict: LOG.debug("Loading command plugins (path=%s)", command_plugins_path) if not command_plugins_path: @@ -50,7 +50,7 @@ def __load_command_plugins( def __load_plugins_and_hardwired_handlers( - config: dict, processor: ProcessorInterface, command_plugins_path: str = None + config: dict, processor: SinkInterface, command_plugins_path: str = None ) -> dict: hardwired_commands_path = create_local_path(os.path.join("commands", "internal")) @@ -61,7 +61,7 @@ def __load_plugins_and_hardwired_handlers( def create_default_command_handler( - config: dict, processor: ProcessorInterface, command_plugins_path: str = None + config: dict, processor: SinkInterface, command_plugins_path: str = None ): return CommandHandler( __load_plugins_and_hardwired_handlers(config, processor, command_plugins_path) diff --git a/src/huemon/commands/hue_command_interface.py b/src/huemon/commands/hue_command_interface.py index de20251..28fd401 100644 --- a/src/huemon/commands/hue_command_interface.py +++ b/src/huemon/commands/hue_command_interface.py @@ -6,11 +6,11 @@ from functools import reduce from huemon.api.api_interface import ApiInterface -from huemon.processors.processor_interface import ProcessorInterface +from huemon.sinks.sink_interface import SinkInterface class HueCommand: - def __init__(self, config: dict, api: ApiInterface, processor: ProcessorInterface): + def __init__(self, config: dict, api: ApiInterface, processor: SinkInterface): self.config = config self.api = api self.processor = processor diff --git a/src/huemon/commands/internal/discover_command.py b/src/huemon/commands/internal/discover_command.py index 906ee56..a7b5998 100644 --- a/src/huemon/commands/internal/discover_command.py +++ b/src/huemon/commands/internal/discover_command.py @@ -12,7 +12,7 @@ from huemon.discoveries.discovery_interface import Discovery from huemon.infrastructure.logger_factory import create_logger from huemon.infrastructure.plugin_loader import load_plugins -from huemon.processors.processor_interface import ProcessorInterface +from huemon.sinks.sink_interface import SinkInterface from huemon.utils.assertions import assert_exists, assert_num_args from huemon.utils.monads.either import Either, rights from huemon.utils.monads.maybe import Maybe, maybe, of @@ -96,7 +96,7 @@ def discover(self, discovery_type): class DiscoverCommand(HueCommand): - def __init__(self, config: dict, api: ApiInterface, processor: ProcessorInterface): + def __init__(self, config: dict, api: ApiInterface, processor: SinkInterface): super().__init__(config, api, processor) self.discovery = Discover(config, api) diff --git a/src/huemon/processors/stdout_processor.py b/src/huemon/processors/stdout_processor.py deleted file mode 100644 index 06831aa..0000000 --- a/src/huemon/processors/stdout_processor.py +++ /dev/null @@ -1,12 +0,0 @@ -from typing import TypeVar - -from huemon.processors.processor_interface import ProcessorInterface -from huemon.utils.errors import HueError -from huemon.utils.monads.either import Either - -TA = TypeVar("TA") - - -class StdoutProcessor(ProcessorInterface): # pylint: disable=too-few-public-methods - def process(self, value: Either[HueError, TA]): - print(value) diff --git a/src/huemon/processors/__init__.py b/src/huemon/sinks/__init__.py similarity index 100% rename from src/huemon/processors/__init__.py rename to src/huemon/sinks/__init__.py diff --git a/src/huemon/processors/processor_interface.py b/src/huemon/sinks/sink_interface.py similarity index 53% rename from src/huemon/processors/processor_interface.py rename to src/huemon/sinks/sink_interface.py index 22e0ca5..256360d 100644 --- a/src/huemon/processors/processor_interface.py +++ b/src/huemon/sinks/sink_interface.py @@ -1,3 +1,8 @@ +# Copyright (c) Ely Deckers. +# +# This source code is licensed under the MPL-2.0 license found in the +# LICENSE file in the root directory of this source tree. + from typing import Generic, TypeVar from huemon.utils.errors import HueError @@ -6,6 +11,6 @@ TA = TypeVar("TA") -class ProcessorInterface(Generic[TA]): # pylint: disable=too-few-public-methods +class SinkInterface(Generic[TA]): # pylint: disable=too-few-public-methods def process(self, value: Either[HueError, TA]): raise NotImplementedError("Processor process implementation missing") diff --git a/src/huemon/sinks/stdout_sink.py b/src/huemon/sinks/stdout_sink.py new file mode 100644 index 0000000..4bc8934 --- /dev/null +++ b/src/huemon/sinks/stdout_sink.py @@ -0,0 +1,17 @@ +# Copyright (c) Ely Deckers. +# +# This source code is licensed under the MPL-2.0 license found in the +# LICENSE file in the root directory of this source tree. + +from typing import TypeVar + +from huemon.sinks.sink_interface import SinkInterface +from huemon.utils.errors import HueError +from huemon.utils.monads.either import Either + +TA = TypeVar("TA") + + +class StdoutSink(SinkInterface): # pylint: disable=too-few-public-methods + def process(self, value: Either[HueError, TA]): + print(value) diff --git a/src/tests/test_command_handler.py b/src/tests/test_command_handler.py index 3e967c5..b8473b2 100644 --- a/src/tests/test_command_handler.py +++ b/src/tests/test_command_handler.py @@ -11,7 +11,7 @@ create_name_to_command_mapping, ) from huemon.commands.internal.system_command import SystemCommand -from huemon.processors.stdout_processor import StdoutProcessor +from huemon.sinks.stdout_sink import StdoutSink from huemon.utils.const import EXIT_FAIL from tests.fixtures import MutableApi, create_system_config @@ -21,9 +21,7 @@ def _ch(system_config: dict): mutable_api.set_system_config(system_config) return CommandHandler( - create_name_to_command_mapping( - {}, mutable_api, StdoutProcessor(), [SystemCommand] - ) + create_name_to_command_mapping({}, mutable_api, StdoutSink(), [SystemCommand]) ) diff --git a/src/tests/test_light_command.py b/src/tests/test_light_command.py index 0a584c1..4ddd566 100644 --- a/src/tests/test_light_command.py +++ b/src/tests/test_light_command.py @@ -12,7 +12,7 @@ create_name_to_command_mapping, ) from huemon.commands.internal.light_command import LightCommand -from huemon.processors.stdout_processor import StdoutProcessor +from huemon.sinks.stdout_sink import StdoutSink from huemon.utils.const import EXIT_FAIL from tests.fixtures import MutableApi @@ -25,9 +25,7 @@ def _ch(lights: List[dict]): mutable_api.set_lights(lights) return CommandHandler( - create_name_to_command_mapping( - {}, mutable_api, StdoutProcessor(), [LightCommand] - ) + create_name_to_command_mapping({}, mutable_api, StdoutSink(), [LightCommand]) ) diff --git a/src/tests/test_sensor_command.py b/src/tests/test_sensor_command.py index e65c4f7..97befb0 100644 --- a/src/tests/test_sensor_command.py +++ b/src/tests/test_sensor_command.py @@ -12,7 +12,7 @@ create_name_to_command_mapping, ) from huemon.commands.internal.sensor_command import SensorCommand -from huemon.processors.stdout_processor import StdoutProcessor +from huemon.sinks.stdout_sink import StdoutSink from tests.fixtures import MutableApi SOME_SENSOR_MAC_0 = "SO:ME:SE:NS:OR:MA:C0" @@ -24,9 +24,7 @@ def _ch(sensors: List[dict]): mutable_api.set_sensors(sensors) return CommandHandler( - create_name_to_command_mapping( - {}, mutable_api, StdoutProcessor(), [SensorCommand] - ) + create_name_to_command_mapping({}, mutable_api, StdoutSink(), [SensorCommand]) ) diff --git a/src/tests/test_system_command.py b/src/tests/test_system_command.py index 049aee2..9058841 100644 --- a/src/tests/test_system_command.py +++ b/src/tests/test_system_command.py @@ -11,7 +11,7 @@ create_name_to_command_mapping, ) from huemon.commands.internal.system_command import SystemCommand -from huemon.processors.stdout_processor import StdoutProcessor +from huemon.sinks.stdout_sink import StdoutSink from tests.fixtures import MutableApi @@ -20,9 +20,7 @@ def _ch(system_config: dict): mutable_api.set_system_config(system_config) return CommandHandler( - create_name_to_command_mapping( - {}, mutable_api, StdoutProcessor(), [SystemCommand] - ) + create_name_to_command_mapping({}, mutable_api, StdoutSink(), [SystemCommand]) ) From 05d0938e1c80d175969d3f0f82ff096d6c32f2a9 Mon Sep 17 00:00:00 2001 From: Ely Deckers Date: Sun, 13 Mar 2022 18:56:35 +0100 Subject: [PATCH 3/7] refactor: handle config read with Either --- src/huemon/__main__.py | 2 +- src/huemon/infrastructure/config_factory.py | 34 +++++++++++++-------- src/huemon/utils/errors.py | 1 + src/huemon/utils/monads/either.py | 18 +++++++++++ 4 files changed, 41 insertions(+), 14 deletions(-) diff --git a/src/huemon/__main__.py b/src/huemon/__main__.py index c75ff48..73f95b9 100755 --- a/src/huemon/__main__.py +++ b/src/huemon/__main__.py @@ -14,7 +14,7 @@ from huemon.utils.errors import exit_fail from huemon.utils.plugins import get_command_plugins_path -CONFIG = create_config() +CONFIG = create_config().if_left({}) LOG = bootstrap_logger(CONFIG) diff --git a/src/huemon/infrastructure/config_factory.py b/src/huemon/infrastructure/config_factory.py index 2213db5..6006516 100644 --- a/src/huemon/infrastructure/config_factory.py +++ b/src/huemon/infrastructure/config_factory.py @@ -9,7 +9,9 @@ import yaml from genericpath import isfile -from huemon.utils.errors import exit_fail +from huemon.utils.errors import E_CODE_CONFIG_NOT_FOUND, HueError +from huemon.utils.monads.either import Either, left, right +from huemon.utils.monads.maybe import Maybe, nothing CONFIG_PATH_LOCAL = path.join(str(Path(__file__).parent.parent), "config.yml") CONFIG_PATH_ENV_VARIABLE = environ.get("HUEMON_CONFIG_PATH") @@ -25,21 +27,27 @@ ) -def __first_existing_config_file(): +def __first_existing_config_file() -> Maybe[str]: for config_path in CONFIG_PATHS_ORDERED_PREFERENCE: if isfile(config_path): - return config_path + return Maybe.of(config_path) - return None + return nothing -def create_config(): - maybe_config_path = __first_existing_config_file() - if not maybe_config_path: - exit_fail( - "No configuration file found in %s", - ",".join(CONFIG_PATHS_ORDERED_PREFERENCE), - ) - - with open(maybe_config_path, "r") as file: +def __read_yaml_file(yaml_path: str): + with open(yaml_path, "r") as file: return yaml.safe_load(file.read()) + + +def create_config() -> Either[HueError, dict]: + return __first_existing_config_file().maybe( + left( + HueError( + code=E_CODE_CONFIG_NOT_FOUND, + message="No configuration file found in %s", + context={"paths": ",".join(CONFIG_PATHS_ORDERED_PREFERENCE)}, + ) + ), + lambda path: right(__read_yaml_file(path)), + ) diff --git a/src/huemon/utils/errors.py b/src/huemon/utils/errors.py index 504175a..52e80a7 100644 --- a/src/huemon/utils/errors.py +++ b/src/huemon/utils/errors.py @@ -12,6 +12,7 @@ LOG = create_logger() E_CODE_PLUGIN_LOADER = -2 +E_CODE_CONFIG_NOT_FOUND = -3 class HueError(NamedTuple): diff --git a/src/huemon/utils/monads/either.py b/src/huemon/utils/monads/either.py index 375216f..645a7e7 100644 --- a/src/huemon/utils/monads/either.py +++ b/src/huemon/utils/monads/either.py @@ -32,9 +32,15 @@ def either(self, map_left: Callable[[TA], TC], map_right: Callable[[TB], TC]) -> def fmap(self, map_: Callable[[TB], TC]) -> Either[TA, TC]: return fmap(self, map_) + def if_left(self, fallback: TB) -> TB: + return if_left(self, fallback) + def is_left(self) -> bool: return is_left(self) + def if_right(self, fallback: TA) -> TA: + return if_right(self, fallback) + def is_right(self) -> bool: return is_right(self) @@ -68,6 +74,10 @@ def __str__(self) -> str: return f"Right(value={self.value.__str__()})" +def __id(value): + return value + + def bind(em0: Either[TC, TA], map_: Callable[[TA], Either[TC, TB]]) -> Either[TC, TB]: if is_left(em0): return cast(Left[TC, TB], em0) @@ -97,6 +107,14 @@ def fmap(em0: Either[TC, TA], map_: Callable[[TA], TB]) -> Either[TC, TB]: return bind(em0, lambda m0: pure(map_(m0))) +def if_left(em0: Either[TA, TB], lft: TB) -> TB: + return either(lambda _: lft, __id, em0) + + +def if_right(em0: Either[TA, TB], rgt: TA) -> TA: + return either(__id, lambda _: rgt, em0) + + def is_left(em0: Either[TA, TB]) -> bool: return isinstance(em0, Left) From 96a27e184e5ea0958ca0d7065e725fc65f8b58cd Mon Sep 17 00:00:00 2001 From: Ely Deckers Date: Sun, 13 Mar 2022 19:22:38 +0100 Subject: [PATCH 4/7] chore: keep raising error for config factory --- src/huemon/__main__.py | 2 +- src/huemon/infrastructure/config_factory.py | 23 +++++++++------------ src/huemon/utils/monads/either.py | 1 - src/huemon/utils/monads/maybe.py | 10 +++++++++ 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/huemon/__main__.py b/src/huemon/__main__.py index 73f95b9..c75ff48 100755 --- a/src/huemon/__main__.py +++ b/src/huemon/__main__.py @@ -14,7 +14,7 @@ from huemon.utils.errors import exit_fail from huemon.utils.plugins import get_command_plugins_path -CONFIG = create_config().if_left({}) +CONFIG = create_config() LOG = bootstrap_logger(CONFIG) diff --git a/src/huemon/infrastructure/config_factory.py b/src/huemon/infrastructure/config_factory.py index 6006516..b693bc1 100644 --- a/src/huemon/infrastructure/config_factory.py +++ b/src/huemon/infrastructure/config_factory.py @@ -9,8 +9,7 @@ import yaml from genericpath import isfile -from huemon.utils.errors import E_CODE_CONFIG_NOT_FOUND, HueError -from huemon.utils.monads.either import Either, left, right +from huemon.utils.errors import exit_fail from huemon.utils.monads.maybe import Maybe, nothing CONFIG_PATH_LOCAL = path.join(str(Path(__file__).parent.parent), "config.yml") @@ -40,14 +39,12 @@ def __read_yaml_file(yaml_path: str): return yaml.safe_load(file.read()) -def create_config() -> Either[HueError, dict]: - return __first_existing_config_file().maybe( - left( - HueError( - code=E_CODE_CONFIG_NOT_FOUND, - message="No configuration file found in %s", - context={"paths": ",".join(CONFIG_PATHS_ORDERED_PREFERENCE)}, - ) - ), - lambda path: right(__read_yaml_file(path)), - ) +def create_config() -> dict: + maybe_config_file = __first_existing_config_file() + if maybe_config_file.is_nothing(): + exit_fail( + "No configuration file found in %s", + ",".join(CONFIG_PATHS_ORDERED_PREFERENCE), + ) + + return __read_yaml_file(maybe_config_file.from_just()) diff --git a/src/huemon/utils/monads/either.py b/src/huemon/utils/monads/either.py index 645a7e7..49565de 100644 --- a/src/huemon/utils/monads/either.py +++ b/src/huemon/utils/monads/either.py @@ -11,7 +11,6 @@ TA = TypeVar("TA") TB = TypeVar("TB") TC = TypeVar("TC") -TD = TypeVar("TD") class Either(Generic[TA, TB]): # pylint: disable=too-few-public-methods diff --git a/src/huemon/utils/monads/maybe.py b/src/huemon/utils/monads/maybe.py index 3503196..8b208e3 100644 --- a/src/huemon/utils/monads/maybe.py +++ b/src/huemon/utils/monads/maybe.py @@ -21,6 +21,9 @@ def bind(self, map_: Callable[[TA], Maybe[TB]]) -> Maybe[TB]: def fmap(self, map_: Callable[[TA], TB]) -> Maybe[TB]: return fmap(self, map_) + def from_just(self) -> TA: + return from_just(self) + def is_nothing(self) -> bool: return is_nothing(self) @@ -76,6 +79,13 @@ def fmap(em0: Maybe[TA], map_: Callable[[TA], TB]) -> Maybe[TB]: return bind(em0, lambda m0: pure(map_(m0))) +def from_just(em0: Maybe[TA]): + if em0.is_nothing(): + raise TypeError("fromJust failed, no instance of Just") + + return em0.value + + def is_nothing(em0: Maybe[TA]) -> bool: return isinstance(em0, Nothing) From 7f1973933e21a66e932e8f2116d9fa5546837142 Mon Sep 17 00:00:00 2001 From: Ely Deckers Date: Sun, 13 Mar 2022 23:24:02 +0100 Subject: [PATCH 5/7] refactor: apply Either error handling for commands --- .vscode/launch.json | 16 +++++++++ src/huemon/commands/internal/agent_command.py | 16 +++++---- .../commands/internal/discover_command.py | 36 +++++++++---------- src/huemon/commands/internal/light_command.py | 6 ++-- src/huemon/discoveries/discovery_interface.py | 30 +++++++++------- .../internal/batteries_discovery.py | 9 +++-- .../discoveries/internal/lights_discovery.py | 7 ++-- .../discoveries/internal/sensors_discovery.py | 4 --- src/huemon/sinks/stdout_sink.py | 7 +++- src/huemon/utils/assertions.py | 28 ++++++++++++++- src/huemon/utils/errors.py | 3 ++ src/huemon/utils/monads/either.py | 8 ++--- src/tests/test_monad_either.py | 4 +-- 13 files changed, 112 insertions(+), 62 deletions(-) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..656e947 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Module", + "type": "python", + "request": "launch", + "module": "huemon", + "args": ["discover", "lights"], + "justMyCode": true + } + ] +} \ No newline at end of file diff --git a/src/huemon/commands/internal/agent_command.py b/src/huemon/commands/internal/agent_command.py index de492a7..7759cb7 100644 --- a/src/huemon/commands/internal/agent_command.py +++ b/src/huemon/commands/internal/agent_command.py @@ -8,7 +8,7 @@ from huemon.api_server import HuemonServerFactory from huemon.commands.hue_command_interface import HueCommand from huemon.infrastructure.logger_factory import create_logger -from huemon.utils.assertions import assert_exists, assert_num_args +from huemon.utils.assertions import assert_exists_e, assert_num_args_e LOG = create_logger() @@ -43,13 +43,17 @@ def name(): def exec(self, arguments): LOG.debug("Running `%s` command (arguments=%s)", AgentCommand.name(), arguments) - assert_num_args(1, arguments, AgentCommand.name()) - action, *_ = arguments - - assert_exists(list(AgentCommand.__SYSTEM_ACTION_MAP), action) + self._process( + assert_num_args_e(1, arguments, AgentCommand.name()) + .bind( + lambda ax: assert_exists_e( + list(AgentCommand.__SYSTEM_ACTION_MAP), ax[0] + ) + ) + .fmap(lambda action: self.__SYSTEM_ACTION_MAP[action](self.config)) + ) - self._process(self.__SYSTEM_ACTION_MAP[action](self.config)) LOG.debug( "Finished `%s` command (arguments=%s)", AgentCommand.name(), arguments ) diff --git a/src/huemon/commands/internal/discover_command.py b/src/huemon/commands/internal/discover_command.py index a7b5998..3849d17 100644 --- a/src/huemon/commands/internal/discover_command.py +++ b/src/huemon/commands/internal/discover_command.py @@ -13,7 +13,7 @@ from huemon.infrastructure.logger_factory import create_logger from huemon.infrastructure.plugin_loader import load_plugins from huemon.sinks.sink_interface import SinkInterface -from huemon.utils.assertions import assert_exists, assert_num_args +from huemon.utils.assertions import assert_exists_e, assert_num_args_e from huemon.utils.monads.either import Either, rights from huemon.utils.monads.maybe import Maybe, maybe, of from huemon.utils.paths import create_local_path @@ -23,9 +23,9 @@ def create_discovery_handlers( - api: ApiInterface, plugins: List[Type[Discovery]] + api: ApiInterface, sink: SinkInterface, plugins: List[Type[Discovery]] ) -> Dict[str, Discovery]: - return reduce(lambda p, c: {**p, c.name(): c(api)}, plugins, {}) + return reduce(lambda p, c: {**p, c.name(): c(api, sink)}, plugins, {}) class DiscoveryHandler: # pylint: disable=too-few-public-methods @@ -40,9 +40,9 @@ def exec(self, discovery_type): ) target, maybe_sub_target, *_ = discovery_type.split(":") + [None] - assert_exists(list(self.handlers), target) - - self.handlers[target].exec([maybe_sub_target] if maybe_sub_target else []) + assert_exists_e(list(self.handlers), target).fmap( + lambda tx: self.handlers[tx] + ).fmap(lambda hlr: hlr.exec([maybe_sub_target] if maybe_sub_target else [])) LOG.debug( "Finished `%s` command (discovery_type=%s)", @@ -52,17 +52,16 @@ def exec(self, discovery_type): class Discover: # pylint: disable=too-few-public-methods - def __init__(self, config: dict, api: ApiInterface): - self.api = api - + def __init__(self, config: dict, api: ApiInterface, sink: SinkInterface): self.discovery_plugins_path = get_discovery_plugins_path(config) - self.handler = self.__create_discovery_handler() + self.handler = self.__create_discovery_handler(api, sink) - def __create_discovery_handler(self): + def __create_discovery_handler(self, api: ApiInterface, sink: SinkInterface): LOG.debug("Loading discovery plugins (path=%s)", self.discovery_plugins_path) discovery_handler_plugins = create_discovery_handlers( - self.api, + api, + sink, rights( Discover.__load_plugins_and_hardwired_handlers( of(self.discovery_plugins_path) @@ -99,7 +98,7 @@ class DiscoverCommand(HueCommand): def __init__(self, config: dict, api: ApiInterface, processor: SinkInterface): super().__init__(config, api, processor) - self.discovery = Discover(config, api) + self.discovery = Discover(config, api, processor) @staticmethod def name(): @@ -109,9 +108,10 @@ def exec(self, arguments): LOG.debug( "Running `%s` command (arguments=%s)", DiscoverCommand.name(), arguments ) - assert_num_args(1, arguments, DiscoverCommand.name()) - - discovery_type, *_ = arguments + assert_num_args_e(1, arguments, DiscoverCommand.name()).fmap( + lambda ax: ax[0] + ).fmap(self.discovery.discover) - self.discovery.discover(discovery_type) - LOG.debug("Finished `discover` command (arguments=%s)", arguments) + LOG.debug( + "Finished `%s` command (arguments=%s)", DiscoverCommand.name(), arguments + ) diff --git a/src/huemon/commands/internal/light_command.py b/src/huemon/commands/internal/light_command.py index ac5dd42..2d06e4d 100644 --- a/src/huemon/commands/internal/light_command.py +++ b/src/huemon/commands/internal/light_command.py @@ -5,7 +5,7 @@ from huemon.commands.hue_command_interface import HueCommand from huemon.infrastructure.logger_factory import create_logger -from huemon.utils.assertions import assert_exists, assert_num_args +from huemon.utils.assertions import assert_exists_e, assert_num_args_e LOG = create_logger() @@ -32,11 +32,11 @@ def name(): def exec(self, arguments): LOG.debug("Running `%s` command (arguments=%s)", LightCommand.name(), arguments) - assert_num_args(2, arguments, LightCommand.name()) + assert_num_args_e(2, arguments, LightCommand.name()) light_id, action = arguments - assert_exists(list(LightCommand.__LIGHT_ACTION_MAP), action) + assert_exists_e(list(LightCommand.__LIGHT_ACTION_MAP), action) self._process( self.__map_light(light_id, LightCommand.__LIGHT_ACTION_MAP[action]) diff --git a/src/huemon/discoveries/discovery_interface.py b/src/huemon/discoveries/discovery_interface.py index 4f6e06a..bad643e 100644 --- a/src/huemon/discoveries/discovery_interface.py +++ b/src/huemon/discoveries/discovery_interface.py @@ -5,13 +5,17 @@ import json from functools import reduce +from typing import List from huemon.api.api_interface import ApiInterface +from huemon.sinks.sink_interface import SinkInterface class Discovery: - def __init__(self, api: ApiInterface): - raise NotImplementedError("Discoveries is missing its required constructor") + def __init__(self, api: ApiInterface, sink: SinkInterface): + self.api = api + + self.sink = sink @staticmethod def _item_to_discovery(item: dict): @@ -29,18 +33,18 @@ def _has_state_field(field: str): ) @staticmethod - def _print_array_as_discovery(items): - print( - json.dumps( - { - "data": reduce( - lambda p, item: [*p, Discovery._item_to_discovery(item)], - items, - [], - ) - } + def _array_as_discovery(items: List[dict]): + return { + "data": reduce( + lambda p, item: [*p, Discovery._item_to_discovery(item)], + items, + [], ) - ) + } + + @staticmethod + def _print_array_as_discovery(items: List[dict]): + print(json.dumps(Discovery._array_as_discovery(items))) @staticmethod def name(): diff --git a/src/huemon/discoveries/internal/batteries_discovery.py b/src/huemon/discoveries/internal/batteries_discovery.py index 108255a..5d4e800 100644 --- a/src/huemon/discoveries/internal/batteries_discovery.py +++ b/src/huemon/discoveries/internal/batteries_discovery.py @@ -3,17 +3,16 @@ # This source code is licensed under the MPL-2.0 license found in the # LICENSE file in the root directory of this source tree. -from huemon.api.api_interface import ApiInterface from huemon.discoveries.discovery_interface import Discovery +from huemon.utils.monads.either import right class BatteriesDiscovery(Discovery): - def __init__(self, api: ApiInterface): - self.api = api - @staticmethod def name(): return "batteries" def exec(self, arguments=None): - Discovery._print_array_as_discovery(self.api.get_batteries()) + self.sink.process( + right(Discovery._array_as_discovery(self.api.get_batteries())) + ) diff --git a/src/huemon/discoveries/internal/lights_discovery.py b/src/huemon/discoveries/internal/lights_discovery.py index ec6ae24..83624a8 100644 --- a/src/huemon/discoveries/internal/lights_discovery.py +++ b/src/huemon/discoveries/internal/lights_discovery.py @@ -3,17 +3,14 @@ # This source code is licensed under the MPL-2.0 license found in the # LICENSE file in the root directory of this source tree. -from huemon.api.api_interface import ApiInterface from huemon.discoveries.discovery_interface import Discovery +from huemon.utils.monads.either import right class LightsDiscovery(Discovery): - def __init__(self, api: ApiInterface): - self.api = api - @staticmethod def name(): return "lights" def exec(self, arguments=None): - Discovery._print_array_as_discovery(self.api.get_lights()) + self.sink.process(right(Discovery._array_as_discovery(self.api.get_lights()))) diff --git a/src/huemon/discoveries/internal/sensors_discovery.py b/src/huemon/discoveries/internal/sensors_discovery.py index 5ef249a..133455f 100644 --- a/src/huemon/discoveries/internal/sensors_discovery.py +++ b/src/huemon/discoveries/internal/sensors_discovery.py @@ -3,7 +3,6 @@ # This source code is licensed under the MPL-2.0 license found in the # LICENSE file in the root directory of this source tree. -from huemon.api.api_interface import ApiInterface from huemon.discoveries.discovery_interface import Discovery from huemon.infrastructure.logger_factory import create_logger from huemon.utils.errors import exit_fail @@ -18,9 +17,6 @@ class SensorsDiscovery(Discovery): - def __init__(self, api: ApiInterface): - self.api = api - @staticmethod def name(): return "sensors" diff --git a/src/huemon/sinks/stdout_sink.py b/src/huemon/sinks/stdout_sink.py index 4bc8934..9857ddb 100644 --- a/src/huemon/sinks/stdout_sink.py +++ b/src/huemon/sinks/stdout_sink.py @@ -3,6 +3,8 @@ # This source code is licensed under the MPL-2.0 license found in the # LICENSE file in the root directory of this source tree. +import json +import sys from typing import TypeVar from huemon.sinks.sink_interface import SinkInterface @@ -14,4 +16,7 @@ class StdoutSink(SinkInterface): # pylint: disable=too-few-public-methods def process(self, value: Either[HueError, TA]): - print(value) + print(value.either(lambda error: error.message, json.dumps)) + + if value.is_left(): + sys.exit(2) diff --git a/src/huemon/utils/assertions.py b/src/huemon/utils/assertions.py index cb6a0d2..39e3d47 100644 --- a/src/huemon/utils/assertions.py +++ b/src/huemon/utils/assertions.py @@ -3,7 +3,13 @@ # This source code is licensed under the MPL-2.0 license found in the # LICENSE file in the root directory of this source tree. -from huemon.utils.errors import exit_fail +from huemon.utils.errors import ( + E_CODE_ASSERT_EXISTS, + E_CODE_ASSERT_NUM_ARGS, + HueError, + exit_fail, +) +from huemon.utils.monads.either import Either, left, right def assert_num_args(expected_number_of_arguments: int, arguments: list, context: str): @@ -19,6 +25,26 @@ def assert_num_args(expected_number_of_arguments: int, arguments: list, context: ) +def assert_num_args_e( + expected_number_of_arguments: int, arguments: list, context: str +) -> Either[HueError, list]: + try: + assert_num_args(expected_number_of_arguments, arguments, context) + + return right(arguments) + except SystemExit: + return left(HueError(E_CODE_ASSERT_NUM_ARGS, "FIXME_NUM_ARGS")) + + def assert_exists(expected_values: list, value: str): if value not in expected_values: exit_fail("Received unknown value `%s` (expected=%s)", value, expected_values) + + +def assert_exists_e(expected_values: list, value: str): + try: + assert_exists(expected_values, value) + + return right(value) + except SystemExit: + return left(HueError(E_CODE_ASSERT_EXISTS, "FIXME_EXISTS")) diff --git a/src/huemon/utils/errors.py b/src/huemon/utils/errors.py index 52e80a7..1b3f359 100644 --- a/src/huemon/utils/errors.py +++ b/src/huemon/utils/errors.py @@ -14,6 +14,9 @@ E_CODE_PLUGIN_LOADER = -2 E_CODE_CONFIG_NOT_FOUND = -3 +E_CODE_ASSERT_NUM_ARGS = -4 +E_CODE_ASSERT_EXISTS = -5 + class HueError(NamedTuple): code: int diff --git a/src/huemon/utils/monads/either.py b/src/huemon/utils/monads/either.py index 49565de..334a7d0 100644 --- a/src/huemon/utils/monads/either.py +++ b/src/huemon/utils/monads/either.py @@ -19,11 +19,11 @@ class Either(Generic[TA, TB]): # pylint: disable=too-few-public-methods def bind(self, map_: Callable[[TB], Either[TA, TC]]) -> Either[TA, TC]: return bind(self, map_) - def chain(self, em1: Either[TA, TB]) -> Either[TA, TB]: - return chain(self, em1) + def then(self, em1: Either[TA, TB]) -> Either[TA, TB]: + return then(self, em1) def discard(self, map_: Callable[[TB], Either[TA, TB]]) -> Either[TA, TB]: - return self.bind(map_).chain(self) + return self.bind(map_).then(self) def either(self, map_left: Callable[[TA], TC], map_right: Callable[[TB], TC]) -> TC: return either(map_left, map_right, self) @@ -88,7 +88,7 @@ def bind(em0: Either[TC, TA], map_: Callable[[TA], Either[TC, TB]]) -> Either[TC return result -def chain(em0: Either[TC, TA], em1: Either[TC, TB]) -> Either[TC, TB]: +def then(em0: Either[TC, TA], em1: Either[TC, TB]) -> Either[TC, TB]: return bind(em0, lambda _: em1) diff --git a/src/tests/test_monad_either.py b/src/tests/test_monad_either.py index b8e0eee..3fbf1b3 100644 --- a/src/tests/test_monad_either.py +++ b/src/tests/test_monad_either.py @@ -85,7 +85,7 @@ def test_when_left_chain_should_not_do_anything(self): either_left = left(0) either_right = right(0) - result = either_left.chain(either_right) + result = either_left.then(either_right) self.assertEqual( either_left, @@ -97,7 +97,7 @@ def test_when_right_chain_should_return_chained_monad(self): either_right_0 = right(21) either_right_1 = right(42) - result = either_right_0.chain(either_right_1) + result = either_right_0.then(either_right_1) self.assertEqual( either_right_1, From b5d83c59668151ad5f75ae08c6d3cacdf59cce91 Mon Sep 17 00:00:00 2001 From: Ely Deckers Date: Sat, 19 Mar 2022 18:15:12 +0100 Subject: [PATCH 6/7] feat: add some sugar to monads --- .../commands/internal/discover_command.py | 22 +++++-- src/huemon/infrastructure/plugin_loader.py | 57 +++++++++++-------- src/huemon/utils/assertions.py | 6 +- src/huemon/utils/common.py | 11 ++++ src/huemon/utils/monads/either.py | 42 +++++++++----- src/huemon/utils/monads/maybe.py | 20 ++++--- 6 files changed, 104 insertions(+), 54 deletions(-) create mode 100644 src/huemon/utils/common.py diff --git a/src/huemon/commands/internal/discover_command.py b/src/huemon/commands/internal/discover_command.py index 3849d17..b1f13a7 100644 --- a/src/huemon/commands/internal/discover_command.py +++ b/src/huemon/commands/internal/discover_command.py @@ -5,7 +5,7 @@ import os from functools import reduce -from typing import Dict, List, Type +from typing import Dict, List, Type, TypeVar from huemon.api.api_interface import ApiInterface from huemon.commands.hue_command_interface import HueCommand @@ -14,13 +14,18 @@ from huemon.infrastructure.plugin_loader import load_plugins from huemon.sinks.sink_interface import SinkInterface from huemon.utils.assertions import assert_exists_e, assert_num_args_e -from huemon.utils.monads.either import Either, rights +from huemon.utils.common import fst +from huemon.utils.errors import HueError +from huemon.utils.monads.either import Either, if_left, if_right, left, right, rights from huemon.utils.monads.maybe import Maybe, maybe, of from huemon.utils.paths import create_local_path from huemon.utils.plugins import get_discovery_plugins_path LOG = create_logger() +TA = TypeVar("TA") +TB = TypeVar("TB") + def create_discovery_handlers( api: ApiInterface, sink: SinkInterface, plugins: List[Type[Discovery]] @@ -104,13 +109,18 @@ def __init__(self, config: dict, api: ApiInterface, processor: SinkInterface): def name(): return "discover" - def exec(self, arguments): + def exec(self, arguments: List[str]): LOG.debug( "Running `%s` command (arguments=%s)", DiscoverCommand.name(), arguments ) - assert_num_args_e(1, arguments, DiscoverCommand.name()).fmap( - lambda ax: ax[0] - ).fmap(self.discovery.discover) + + error, param = assert_num_args_e(1, arguments, DiscoverCommand.name()) | fst + + if error: + self.processor.process(error) + return + + self.discovery.discover(param) LOG.debug( "Finished `%s` command (arguments=%s)", DiscoverCommand.name(), arguments diff --git a/src/huemon/infrastructure/plugin_loader.py b/src/huemon/infrastructure/plugin_loader.py index 055cc42..536febb 100644 --- a/src/huemon/infrastructure/plugin_loader.py +++ b/src/huemon/infrastructure/plugin_loader.py @@ -5,11 +5,14 @@ import importlib.util import inspect +from importlib.machinery import ModuleSpec +from modulefinder import Module from pathlib import Path from typing import List, Tuple, Type, TypeVar, cast +from huemon.utils.common import fst, snd from huemon.utils.errors import E_CODE_PLUGIN_LOADER, HueError -from huemon.utils.monads.either import Either, Left, right +from huemon.utils.monads.either import Either, left, right from huemon.utils.monads.maybe import Maybe TA = TypeVar("TA") @@ -19,8 +22,33 @@ def __error(message: str) -> HueError: return HueError(E_CODE_PLUGIN_LOADER, message) -def __lerror(message: str) -> Left[HueError, TA]: - return Left[HueError, TA](__error(message)) +def __lerror(message: str) -> Either[HueError, TA]: + return left(__error(message)) + + +def __filter_subclasses_from_module(sub_class): + return ( + lambda member: inspect.isclass(member) + and issubclass(member, sub_class) + and member is not sub_class + ) + + +def __read_members_from_module(sub_class): + return lambda module: list( + map( + lambda obj: cast(Tuple[str, Type[TA]], obj), + inspect.getmembers(module, __filter_subclasses_from_module(sub_class)), + ), + ) + + +def __load_module(sm: Tuple[ModuleSpec, Module]): + spec, module = sm + + Maybe.of(spec.loader).fmap(lambda loader: loader.exec_module(module)) + + return Either.pure(module) def __get_plugin_type( @@ -43,27 +71,8 @@ def __get_plugin_type( lambda module: right((spec, module)), ), ) - .discard( - lambda sm: Either.pure( - Maybe.of(sm[0].loader).bind( - lambda loader: Maybe.of(loader.exec_module(sm[1])) - ) - ) - ) - .fmap(lambda sm: sm[1]) - .fmap( - lambda module: list( - map( - lambda obj: cast(Tuple[str, Type[TA]], obj), - filter( - lambda member: inspect.isclass(member[1]) - and issubclass(member[1], sub_class) - and member[1] is not sub_class, - inspect.getmembers(module), - ), - ), - ), - ) + .bind(__load_module) + .fmap(__read_members_from_module(sub_class)) .bind( lambda plugin_types: Maybe.of(len(plugin_types) > 0).maybe( __lerror(f"No plugin of type `{sub_class}` found"), diff --git a/src/huemon/utils/assertions.py b/src/huemon/utils/assertions.py index 39e3d47..23b0886 100644 --- a/src/huemon/utils/assertions.py +++ b/src/huemon/utils/assertions.py @@ -3,6 +3,8 @@ # This source code is licensed under the MPL-2.0 license found in the # LICENSE file in the root directory of this source tree. +from typing import List + from huemon.utils.errors import ( E_CODE_ASSERT_EXISTS, E_CODE_ASSERT_NUM_ARGS, @@ -26,8 +28,8 @@ def assert_num_args(expected_number_of_arguments: int, arguments: list, context: def assert_num_args_e( - expected_number_of_arguments: int, arguments: list, context: str -) -> Either[HueError, list]: + expected_number_of_arguments: int, arguments: List[str], context: str +) -> Either[HueError, List[str]]: try: assert_num_args(expected_number_of_arguments, arguments, context) diff --git a/src/huemon/utils/common.py b/src/huemon/utils/common.py new file mode 100644 index 0000000..89c4aeb --- /dev/null +++ b/src/huemon/utils/common.py @@ -0,0 +1,11 @@ +from typing import Sequence, TypeVar + +TA = TypeVar("TA") + + +def fst(seq: Sequence[TA]) -> TA: + return seq[0] + + +def snd(seq: Sequence[TA]) -> TA: + return seq[1] diff --git a/src/huemon/utils/monads/either.py b/src/huemon/utils/monads/either.py index 334a7d0..92b21e7 100644 --- a/src/huemon/utils/monads/either.py +++ b/src/huemon/utils/monads/either.py @@ -6,16 +6,34 @@ from __future__ import annotations from argparse import ArgumentTypeError -from typing import Callable, Generic, List, TypeVar, Union, cast +from typing import Callable, List, Tuple, TypeVar, Union, cast TA = TypeVar("TA") TB = TypeVar("TB") TC = TypeVar("TC") -class Either(Generic[TA, TB]): # pylint: disable=too-few-public-methods +class Either(Tuple[TA, TB]): # pylint: disable=too-few-public-methods value: Union[TA, TB] + def __new__(cls, _): + return super(Either, cls).__new__(cls) + + def __init__(self, value: Union[TA, TB]): + self.value = value + + def __len__(self): + return 2 + + def __iter__(self): + return (self.if_right(None), self.if_left(None)).__iter__() + + def __or__(self, map_: Callable[[TB], TC]) -> Either[TA, TC]: + return self.fmap(map_) + + def __ge__(self, map_: Callable[[TB], Either[TA, TC]]) -> Either[TA, TC]: + return bind(self, map_) + def bind(self, map_: Callable[[TB], Either[TA, TC]]) -> Either[TA, TC]: return bind(self, map_) @@ -57,18 +75,12 @@ def __eq__(self, __o: object) -> bool: return __o.value == self.value -class Left(Either[TA, TB]): # pylint: disable=too-few-public-methods - def __init__(self, value: TA): - self.value = value - +class _Left(Either[TA, TB]): # pylint: disable=too-few-public-methods def __str__(self) -> str: return f"Left(value={self.value.__str__()})" -class Right(Either[TA, TB]): # pylint: disable=too-few-public-methods - def __init__(self, value: TB): - self.value = value - +class _Right(Either[TA, TB]): # pylint: disable=too-few-public-methods def __str__(self) -> str: return f"Right(value={self.value.__str__()})" @@ -79,7 +91,7 @@ def __id(value): def bind(em0: Either[TC, TA], map_: Callable[[TA], Either[TC, TB]]) -> Either[TC, TB]: if is_left(em0): - return cast(Left[TC, TB], em0) + return cast(_Left[TC, TB], em0) result = map_(cast(TA, em0.value)) if not isinstance(result, Either): @@ -115,7 +127,7 @@ def if_right(em0: Either[TA, TB], rgt: TA) -> TA: def is_left(em0: Either[TA, TB]) -> bool: - return isinstance(em0, Left) + return isinstance(em0, _Left) def is_right(em0: Either[TA, TB]) -> bool: @@ -123,7 +135,7 @@ def is_right(em0: Either[TA, TB]) -> bool: def left(value: TA) -> Either[TA, TB]: - return Left(value) + return _Left(value) def lefts(eithers: List[Either[TA, TB]]) -> List[TA]: @@ -135,7 +147,7 @@ def rights(eithers: List[Either[TA, TB]]) -> List[TB]: def right(value: TB) -> Either[TA, TB]: - return Right(value) + return pure(value) -pure = Right # pylint: disable=invalid-name +pure = _Right # pylint: disable=invalid-name diff --git a/src/huemon/utils/monads/maybe.py b/src/huemon/utils/monads/maybe.py index 8b208e3..f97cdb9 100644 --- a/src/huemon/utils/monads/maybe.py +++ b/src/huemon/utils/monads/maybe.py @@ -15,6 +15,12 @@ class Maybe(Generic[TA]): # pylint: disable=too-few-public-methods value: TA + def __or__(self, map_: Callable[[TA], TB]) -> Maybe[TA, TB]: + return self.fmap(map_) + + def __ge__(self, map_: Callable[[TA], Maybe[TB]]) -> Maybe[TB]: + return bind(self, map_) + def bind(self, map_: Callable[[TA], Maybe[TB]]) -> Maybe[TB]: return bind(self, map_) @@ -28,7 +34,7 @@ def is_nothing(self) -> bool: return is_nothing(self) @staticmethod - def of(value: TA): # pylint: disable=invalid-name + def of(value: TA | None) -> Maybe[TA]: # pylint: disable=invalid-name return of(value) def maybe(self: Maybe[TA], fallback: TB, map_: Callable[[TA], TB]) -> TB: @@ -44,7 +50,7 @@ def __eq__(self, __o: object) -> bool: return __o.value == self.value -class Just(Maybe[TA]): # pylint: disable=too-few-public-methods +class _Just(Maybe[TA]): # pylint: disable=too-few-public-methods value: TA def __init__(self, value: TA): @@ -54,14 +60,14 @@ def __str__(self) -> str: return f"Just(value={self.value.__str__()})" -class Nothing(Maybe[TA]): # pylint: disable=too-few-public-methods +class _Nothing(Maybe[TA]): # pylint: disable=too-few-public-methods def __str__(self) -> str: return "Nothing" -nothing: Maybe = Nothing() +nothing: Maybe = _Nothing() -pure = Just # pylint: disable=invalid-name +pure = _Just # pylint: disable=invalid-name def bind(em0: Maybe[TA], map_: Callable[[TA], Maybe[TB]]) -> Maybe[TB]: @@ -87,12 +93,12 @@ def from_just(em0: Maybe[TA]): def is_nothing(em0: Maybe[TA]) -> bool: - return isinstance(em0, Nothing) + return isinstance(em0, _Nothing) def maybe(fallback: TB, map_: Callable[[TA], TB], em0: Maybe[TA]) -> TB: return fallback if is_nothing(em0) else map_(em0.value) -def of(value: TA): # pylint: disable=invalid-name +def of(value: TA | None) -> Maybe[TA]: # pylint: disable=invalid-name return nothing if not value else pure(value) From 632ba7217ea41ba2ba93b02b4524a422981965b0 Mon Sep 17 00:00:00 2001 From: Ely Deckers Date: Sat, 19 Mar 2022 20:32:41 +0100 Subject: [PATCH 7/7] chore: add some type checker exceptions --- .../commands/internal/discover_command.py | 7 ++- src/huemon/infrastructure/plugin_loader.py | 59 ++++++++++--------- src/huemon/utils/assertions.py | 4 +- src/huemon/utils/common.py | 6 +- src/huemon/utils/monads/either.py | 16 ++--- src/huemon/utils/monads/maybe.py | 2 +- 6 files changed, 49 insertions(+), 45 deletions(-) diff --git a/src/huemon/commands/internal/discover_command.py b/src/huemon/commands/internal/discover_command.py index b1f13a7..4117bd8 100644 --- a/src/huemon/commands/internal/discover_command.py +++ b/src/huemon/commands/internal/discover_command.py @@ -15,8 +15,7 @@ from huemon.sinks.sink_interface import SinkInterface from huemon.utils.assertions import assert_exists_e, assert_num_args_e from huemon.utils.common import fst -from huemon.utils.errors import HueError -from huemon.utils.monads.either import Either, if_left, if_right, left, right, rights +from huemon.utils.monads.either import Either, rights from huemon.utils.monads.maybe import Maybe, maybe, of from huemon.utils.paths import create_local_path from huemon.utils.plugins import get_discovery_plugins_path @@ -114,7 +113,9 @@ def exec(self, arguments: List[str]): "Running `%s` command (arguments=%s)", DiscoverCommand.name(), arguments ) - error, param = assert_num_args_e(1, arguments, DiscoverCommand.name()) | fst + error, param = assert_num_args_e(1, arguments, DiscoverCommand.name()).fmap( + fst # type: ignore + ) if error: self.processor.process(error) diff --git a/src/huemon/infrastructure/plugin_loader.py b/src/huemon/infrastructure/plugin_loader.py index 536febb..8441ebc 100644 --- a/src/huemon/infrastructure/plugin_loader.py +++ b/src/huemon/infrastructure/plugin_loader.py @@ -6,23 +6,23 @@ import importlib.util import inspect from importlib.machinery import ModuleSpec -from modulefinder import Module from pathlib import Path +from types import ModuleType from typing import List, Tuple, Type, TypeVar, cast -from huemon.utils.common import fst, snd from huemon.utils.errors import E_CODE_PLUGIN_LOADER, HueError from huemon.utils.monads.either import Either, left, right from huemon.utils.monads.maybe import Maybe TA = TypeVar("TA") +TB = TypeVar("TB") def __error(message: str) -> HueError: return HueError(E_CODE_PLUGIN_LOADER, message) -def __lerror(message: str) -> Either[HueError, TA]: +def __lerror(message: str) -> Either[HueError, TB]: return left(__error(message)) @@ -43,8 +43,8 @@ def __read_members_from_module(sub_class): ) -def __load_module(sm: Tuple[ModuleSpec, Module]): - spec, module = sm +def __load_module(spec_and_module: Tuple[ModuleSpec, ModuleType]): + spec, module = spec_and_module Maybe.of(spec.loader).fmap(lambda loader: loader.exec_module(module)) @@ -54,30 +54,33 @@ def __load_module(sm: Tuple[ModuleSpec, Module]): def __get_plugin_type( module_name: str, path: str, sub_class: Type[TA] ) -> Either[HueError, Type[TA]]: - return ( - Maybe.of(importlib.util.spec_from_file_location(module_name, path)) - .maybe( - __lerror("ModuleSpec could not be loaded from the provided path"), - right, - ) - .bind( - lambda spec: Maybe.of(spec.loader).maybe( - __lerror("ModuleSpec has no loader"), lambda _: right(spec) - ) - ) - .bind( - lambda spec: Maybe.of(importlib.util.module_from_spec(spec)).maybe( - __lerror("Module could not be loaded from ModuleSpec"), - lambda module: right((spec, module)), - ), + + maybe_spec = Maybe.of(importlib.util.spec_from_file_location(module_name, path)) + + error_or_spec: Either[HueError, ModuleSpec] = maybe_spec.maybe( + __lerror("ModuleSpec could not be loaded from the provided path"), + right, # type: ignore + ).bind( + lambda spec: Maybe.of(spec.loader).maybe( # type: ignore + __lerror("ModuleSpec has no loader"), lambda _: right(spec) ) - .bind(__load_module) - .fmap(__read_members_from_module(sub_class)) - .bind( - lambda plugin_types: Maybe.of(len(plugin_types) > 0).maybe( - __lerror(f"No plugin of type `{sub_class}` found"), - lambda _: right(plugin_types[0][1]), - ) + ) + + error_or_module: Either[ + HueError, Tuple[ModuleSpec, ModuleType] + ] = error_or_spec.bind( + lambda spec: Maybe.of(importlib.util.module_from_spec(spec)).maybe( + __lerror("Module could not be loaded from ModuleSpec"), + lambda module: right((spec, module)), # type: ignore + ), + ).bind( + __load_module + ) + + return error_or_module.fmap(__read_members_from_module(sub_class)).bind( + lambda plugin_types: Maybe.of(len(plugin_types) > 0).maybe( + __lerror(f"No plugin of type `{sub_class}` found"), + lambda _: right(plugin_types[0][1]), ) ) diff --git a/src/huemon/utils/assertions.py b/src/huemon/utils/assertions.py index 23b0886..4170b74 100644 --- a/src/huemon/utils/assertions.py +++ b/src/huemon/utils/assertions.py @@ -3,7 +3,7 @@ # This source code is licensed under the MPL-2.0 license found in the # LICENSE file in the root directory of this source tree. -from typing import List +from typing import List, Sequence from huemon.utils.errors import ( E_CODE_ASSERT_EXISTS, @@ -29,7 +29,7 @@ def assert_num_args(expected_number_of_arguments: int, arguments: list, context: def assert_num_args_e( expected_number_of_arguments: int, arguments: List[str], context: str -) -> Either[HueError, List[str]]: +) -> Either[HueError, Sequence[str]]: try: assert_num_args(expected_number_of_arguments, arguments, context) diff --git a/src/huemon/utils/common.py b/src/huemon/utils/common.py index 89c4aeb..f334596 100644 --- a/src/huemon/utils/common.py +++ b/src/huemon/utils/common.py @@ -1,11 +1,11 @@ from typing import Sequence, TypeVar -TA = TypeVar("TA") +TS = TypeVar("TS") -def fst(seq: Sequence[TA]) -> TA: +def fst(seq: Sequence[TS]) -> TS: return seq[0] -def snd(seq: Sequence[TA]) -> TA: +def snd(seq: Sequence[TS]) -> TS: return seq[1] diff --git a/src/huemon/utils/monads/either.py b/src/huemon/utils/monads/either.py index 92b21e7..655c41f 100644 --- a/src/huemon/utils/monads/either.py +++ b/src/huemon/utils/monads/either.py @@ -6,18 +6,18 @@ from __future__ import annotations from argparse import ArgumentTypeError -from typing import Callable, List, Tuple, TypeVar, Union, cast +from typing import Callable, Generic, List, TypeVar, Union, cast TA = TypeVar("TA") TB = TypeVar("TB") TC = TypeVar("TC") -class Either(Tuple[TA, TB]): # pylint: disable=too-few-public-methods +class Either(Generic[TA, TB]): # pylint: disable=too-few-public-methods value: Union[TA, TB] - def __new__(cls, _): - return super(Either, cls).__new__(cls) + # def __new__(cls, _): + # return super(Either, cls).__new__(cls) def __init__(self, value: Union[TA, TB]): self.value = value @@ -28,7 +28,7 @@ def __len__(self): def __iter__(self): return (self.if_right(None), self.if_left(None)).__iter__() - def __or__(self, map_: Callable[[TB], TC]) -> Either[TA, TC]: + def __or__(self, map_: Callable[[TB], TC]): return self.fmap(map_) def __ge__(self, map_: Callable[[TB], Either[TA, TC]]) -> Either[TA, TC]: @@ -114,7 +114,7 @@ def either( ) -def fmap(em0: Either[TC, TA], map_: Callable[[TA], TB]) -> Either[TC, TB]: +def fmap(em0: Either[TA, TB], map_: Callable[[TB], TC]) -> Either[TA, TC]: return bind(em0, lambda m0: pure(map_(m0))) @@ -147,7 +147,7 @@ def rights(eithers: List[Either[TA, TB]]) -> List[TB]: def right(value: TB) -> Either[TA, TB]: - return pure(value) + return _Right(value) -pure = _Right # pylint: disable=invalid-name +pure = right = _Right # pylint: disable=invalid-name diff --git a/src/huemon/utils/monads/maybe.py b/src/huemon/utils/monads/maybe.py index f97cdb9..eb64417 100644 --- a/src/huemon/utils/monads/maybe.py +++ b/src/huemon/utils/monads/maybe.py @@ -15,7 +15,7 @@ class Maybe(Generic[TA]): # pylint: disable=too-few-public-methods value: TA - def __or__(self, map_: Callable[[TA], TB]) -> Maybe[TA, TB]: + def __or__(self, map_: Callable[[TA], TB]) -> Maybe[TB]: return self.fmap(map_) def __ge__(self, map_: Callable[[TA], Maybe[TB]]) -> Maybe[TB]: