From 4bbf00278d1257af8d1915197097ca3abd28f8d2 Mon Sep 17 00:00:00 2001 From: Benjamin Braatz Date: Sun, 21 Mar 2021 00:08:56 +0100 Subject: [PATCH] Document and test util.py. --- controlpi-plugins/util.py | 231 +++++++++++++++++++++++++++++++++----- 1 file changed, 205 insertions(+), 26 deletions(-) diff --git a/controlpi-plugins/util.py b/controlpi-plugins/util.py index d1e014a..284e15f 100644 --- a/controlpi-plugins/util.py +++ b/controlpi-plugins/util.py @@ -7,25 +7,22 @@ >>> import asyncio >>> import controlpi >>> asyncio.run(controlpi.test( -... {"Test Log": {"plugin": "Log", -... "filter": [{"sender": {"const": "Test Alias"}}]}, -... "Test Init": {"plugin": "Init", -... "messages": [{"id": 42, -... "content": "Test Message"}]}, -... "Test Alias": {"plugin": "Alias", -... "from": {"sender": {"const": "Test Init"}, -... "id": {"const": 42}}, -... "to": {"id": "translated"}}}, -... [])) # doctest: +NORMALIZE_WHITESPACE +... {"Test Log": {"plugin": "Log", +... "filter": [{"sender": {"const": "Test Alias"}}]}, +... "Test Init": {"plugin": "Init", +... "messages": [{"id": 42, "content": "Test Message"}]}, +... "Test Alias": {"plugin": "Alias", +... "from": {"sender": {"const": "Test Init"}, +... "id": {"const": 42}}, +... "to": {"id": "translated"}}}, [])) +... # doctest: +NORMALIZE_WHITESPACE test(): {'sender': '', 'event': 'registered', 'client': 'Test Log', 'plugin': 'Log', 'sends': [], 'receives': [{'sender': {'const': 'Test Alias'}}]} test(): {'sender': '', 'event': 'registered', 'client': 'Test Init', 'plugin': 'Init', 'sends': [{'id': {'const': 42}, - 'content': {'const': 'Test Message'}}, - {'target': {'const': 'Test Init'}, - 'command': {'const': 'execute'}}], + 'content': {'const': 'Test Message'}}], 'receives': [{'target': {'const': 'Test Init'}, 'command': {'const': 'execute'}}]} test(): {'sender': '', 'event': 'registered', @@ -33,15 +30,12 @@ test(): {'sender': '', 'event': 'registered', 'sends': [{'id': {'const': 'translated'}}], 'receives': [{'sender': {'const': 'Test Init'}, 'id': {'const': 42}}]} -test(): {'sender': 'Test Init', 'target': 'Test Init', 'command': 'execute'} test(): {'sender': 'Test Init', 'id': 42, 'content': 'Test Message'} test(): {'sender': 'Test Alias', 'id': 'translated', 'content': 'Test Message'} Test Log: {'sender': 'Test Alias', 'id': 'translated', 'content': 'Test Message'} - -TODO: documentation, doctests """ from controlpi import BasePlugin, Message, MessageTemplate @@ -49,15 +43,68 @@ from controlpi import BasePlugin, Message, MessageTemplate class Log(BasePlugin): """Log messages on stdout. + The "filter" configuration key gets a list of message templates defining + the messages that should be logged by the plugin instance. + + In the following example the first and third message match the given + template and are logged by the instance "Test Log", while the second + message does not match and is only logged by the test, but not by the + Log instance: + >>> import asyncio + >>> import controlpi + >>> asyncio.run(controlpi.test( + ... {"Test Log": {"plugin": "Log", + ... "filter": [{"id": {"const": 42}}]}}, + ... [{"id": 42, "message": "Test Message"}, + ... {"id": 42.42, "message": "Second Message"}, + ... {"id": 42, "message": "Third Message"}])) + ... # doctest: +NORMALIZE_WHITESPACE + test(): {'sender': '', 'event': 'registered', + 'client': 'Test Log', 'plugin': 'Log', + 'sends': [], 'receives': [{'id': {'const': 42}}]} + test(): {'sender': 'test()', 'id': 42, 'message': 'Test Message'} + Test Log: {'sender': 'test()', 'id': 42, 'message': 'Test Message'} + test(): {'sender': 'test()', 'id': 42.42, 'message': 'Second Message'} + test(): {'sender': 'test()', 'id': 42, 'message': 'Third Message'} + Test Log: {'sender': 'test()', 'id': 42, 'message': 'Third Message'} + + The "filter" key is required: + >>> asyncio.run(controlpi.test( + ... {"Test Log": {"plugin": "Log"}}, [])) + 'filter' is a required property + + Failed validating 'required' in schema: + {'properties': {'filter': {'items': {'type': 'object'}, + 'type': 'array'}}, + 'required': ['filter']} + + On instance: + {'plugin': 'Log'} + Configuration for 'Test Log' is not valid. + + The "filter" key has to contain a list of message templates, i.e., + JSON objects: + >>> asyncio.run(controlpi.test( + ... {"Test Log": {"plugin": "Log", + ... "filter": [42]}}, [])) + 42 is not of type 'object' + + Failed validating 'type' in schema['properties']['filter']['items']: + {'type': 'object'} + + On instance['filter'][0]: + 42 + Configuration for 'Test Log' is not valid. """ - CONF_SCHEMA = {'properties': {'filter': {'type': 'array'}}, + CONF_SCHEMA = {'properties': {'filter': {'type': 'array', + 'items': {'type': 'object'}}}, 'required': ['filter']} """Schema for Log plugin configuration. Required configuration key: - - 'filter' with list of message templates to be logged. + - 'filter': list of message templates to be logged. """ async def log(self, message: Message) -> None: @@ -76,15 +123,74 @@ class Log(BasePlugin): class Init(BasePlugin): """Send list of messages on startup and on demand. + The "messages" configuration key gets a list of messages to be sent on + startup. The same list is sent in reaction to a message with + "target": and "command": "execute". + + In the example, the two configured messages are sent twice, once at + startup and a second time in reaction to the "execute" command sent by + the test: + >>> import asyncio + >>> import controlpi + >>> asyncio.run(controlpi.test( + ... {"Test Init": {"plugin": "Init", + ... "messages": [{"id": 42, + ... "content": "Test Message"}, + ... {"id": 42.42, + ... "content": "Second Message"}]}}, + ... [{"target": "Test Init", "command": "execute"}])) + ... # doctest: +NORMALIZE_WHITESPACE + test(): {'sender': '', 'event': 'registered', + 'client': 'Test Init', 'plugin': 'Init', + 'sends': [{'id': {'const': 42}, + 'content': {'const': 'Test Message'}}, + {'id': {'const': 42.42}, + 'content': {'const': 'Second Message'}}], + 'receives': [{'target': {'const': 'Test Init'}, + 'command': {'const': 'execute'}}]} + test(): {'sender': 'Test Init', 'id': 42, 'content': 'Test Message'} + test(): {'sender': 'Test Init', 'id': 42.42, 'content': 'Second Message'} + test(): {'sender': 'test()', 'target': 'Test Init', 'command': 'execute'} + test(): {'sender': 'Test Init', 'id': 42, 'content': 'Test Message'} + test(): {'sender': 'Test Init', 'id': 42.42, 'content': 'Second Message'} + + The "messages" key is required: + >>> asyncio.run(controlpi.test( + ... {"Test Init": {"plugin": "Init"}}, [])) + 'messages' is a required property + + Failed validating 'required' in schema: + {'properties': {'messages': {'items': {'type': 'object'}, + 'type': 'array'}}, + 'required': ['messages']} + + On instance: + {'plugin': 'Init'} + Configuration for 'Test Init' is not valid. + + The "messages" key has to contain a list of (partial) messages, i.e., + JSON objects: + >>> asyncio.run(controlpi.test( + ... {"Test Init": {"plugin": "Init", + ... "messages": [42]}}, [])) + 42 is not of type 'object' + + Failed validating 'type' in schema['properties']['messages']['items']: + {'type': 'object'} + + On instance['messages'][0]: + 42 + Configuration for 'Test Init' is not valid. """ - CONF_SCHEMA = {'properties': {'messages': {'type': 'array'}}, + CONF_SCHEMA = {'properties': {'messages': {'type': 'array', + 'items': {'type': 'object'}}}, 'required': ['messages']} """Schema for Init plugin configuration. Required configuration key: - - 'messages' with list of messages to be sent. + - 'messages': list of messages to be sent. """ async def execute(self, message: Message) -> None: @@ -98,18 +204,91 @@ class Init(BasePlugin): 'command': {'const': 'execute'}})] sends = [MessageTemplate.from_message(message) for message in self.conf['messages']] - sends.extend(receives) self.bus.register(self.name, 'Init', sends, receives, self.execute) async def run(self) -> None: - """Send execution command on startup.""" - await self.bus.send(Message(self.name, {'target': self.name, - 'command': 'execute'})) + """Send configured messages on startup.""" + for message in self.conf['messages']: + await self.bus.send(Message(self.name, message)) class Alias(BasePlugin): """Translate messages to an alias. + The "from" configuration key gets a message template and the + configuration key "to" a (partial) message. All messages matching the + template are received by the Alias instance and a message translated by + removing the keys of the "from" template and adding the keys and values + of the "to" message is sent. Keys that are neither in "from" nor in "to" + are retained. + + In the example, the two messages sent by the test are translated by the + Alias instance and the translated messages are sent by it preserving + the "content" keys: + >>> import asyncio + >>> import controlpi + >>> asyncio.run(controlpi.test( + ... {"Test Alias": {"plugin": "Alias", + ... "from": {"id": {"const": 42}}, + ... "to": {"id": "translated"}}}, + ... [{"id": 42, "content": "Test Message"}, + ... {"id": 42, "content": "Second Message"}])) + ... # doctest: +NORMALIZE_WHITESPACE + test(): {'sender': '', 'event': 'registered', + 'client': 'Test Alias', 'plugin': 'Alias', + 'sends': [{'id': {'const': 'translated'}}], + 'receives': [{'id': {'const': 42}}]} + test(): {'sender': 'test()', 'id': 42, + 'content': 'Test Message'} + test(): {'sender': 'test()', 'id': 42, + 'content': 'Second Message'} + test(): {'sender': 'Test Alias', 'id': 'translated', + 'content': 'Test Message'} + test(): {'sender': 'Test Alias', 'id': 'translated', + 'content': 'Second Message'} + + The "from" and "to" keys are required: + >>> asyncio.run(controlpi.test( + ... {"Test Alias": {"plugin": "Alias"}}, [])) + 'from' is a required property + + Failed validating 'required' in schema: + {'properties': {'from': {'type': 'object'}, 'to': {'type': 'object'}}, + 'required': ['from', 'to']} + + On instance: + {'plugin': 'Alias'} + 'to' is a required property + + Failed validating 'required' in schema: + {'properties': {'from': {'type': 'object'}, 'to': {'type': 'object'}}, + 'required': ['from', 'to']} + + On instance: + {'plugin': 'Alias'} + Configuration for 'Test Alias' is not valid. + + The "from" key has to contain a message template and the "to" key a + (partial) message, i.e., both have to be JSON objects: + >>> asyncio.run(controlpi.test( + ... {"Test Alias": {"plugin": "Alias", + ... "from": 42, + ... "to": 42}}, [])) + 42 is not of type 'object' + + Failed validating 'type' in schema['properties']['from']: + {'type': 'object'} + + On instance['from']: + 42 + 42 is not of type 'object' + + Failed validating 'type' in schema['properties']['to']: + {'type': 'object'} + + On instance['to']: + 42 + Configuration for 'Test Alias' is not valid. """ CONF_SCHEMA = {'properties': {'from': {'type': 'object'}, @@ -119,8 +298,8 @@ class Alias(BasePlugin): Required configuration keys: - - 'from' with template of messages to be translated. - - 'to' with translated message to be sent. + - 'from': template of messages to be translated. + - 'to': translated message to be sent. """ async def alias(self, message: Message) -> None: -- 2.34.1