Document and test util.py.
authorBenjamin Braatz <bb@bbraatz.eu>
Sat, 20 Mar 2021 23:08:56 +0000 (00:08 +0100)
committerBenjamin Braatz <bb@bbraatz.eu>
Sat, 20 Mar 2021 23:08:56 +0000 (00:08 +0100)
controlpi-plugins/util.py

index d1e014a9cefec2a6ac9be446a9ec1dac8f6521ae..284e15f2ab2625faa4a20dcdfa3fd7cdc49787f0 100644 (file)
@@ -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
+    <BLANKLINE>
+    Failed validating 'required' in schema:
+        {'properties': {'filter': {'items': {'type': 'object'},
+                                   'type': 'array'}},
+         'required': ['filter']}
+    <BLANKLINE>
+    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'
+    <BLANKLINE>
+    Failed validating 'type' in schema['properties']['filter']['items']:
+        {'type': 'object'}
+    <BLANKLINE>
+    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": <name> 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
+    <BLANKLINE>
+    Failed validating 'required' in schema:
+        {'properties': {'messages': {'items': {'type': 'object'},
+                                     'type': 'array'}},
+         'required': ['messages']}
+    <BLANKLINE>
+    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'
+    <BLANKLINE>
+    Failed validating 'type' in schema['properties']['messages']['items']:
+        {'type': 'object'}
+    <BLANKLINE>
+    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
+    <BLANKLINE>
+    Failed validating 'required' in schema:
+        {'properties': {'from': {'type': 'object'}, 'to': {'type': 'object'}},
+         'required': ['from', 'to']}
+    <BLANKLINE>
+    On instance:
+        {'plugin': 'Alias'}
+    'to' is a required property
+    <BLANKLINE>
+    Failed validating 'required' in schema:
+        {'properties': {'from': {'type': 'object'}, 'to': {'type': 'object'}},
+         'required': ['from', 'to']}
+    <BLANKLINE>
+    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'
+    <BLANKLINE>
+    Failed validating 'type' in schema['properties']['from']:
+        {'type': 'object'}
+    <BLANKLINE>
+    On instance['from']:
+        42
+    42 is not of type 'object'
+    <BLANKLINE>
+    Failed validating 'type' in schema['properties']['to']:
+        {'type': 'object'}
+    <BLANKLINE>
+    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: