Improvements in Message and Alias.
authorBenjamin Braatz <benjamin.braatz@graph-it.com>
Wed, 22 Sep 2021 12:22:16 +0000 (14:22 +0200)
committerBenjamin Braatz <benjamin.braatz@graph-it.com>
Wed, 22 Sep 2021 12:22:16 +0000 (14:22 +0200)
* Explicitly given sender has higher priortity in Message constructor.
* Alias retains everything except explicitly modified keys in 'to' and
  'translate'.

controlpi/messagebus.py
controlpi_plugins/util.py

index b320245182874f00788cdea8f3a51d3898331863..4c61817017bedcf1850cb30c918ac93a8a6b3f05 100644 (file)
@@ -145,15 +145,30 @@ class Message(Dict[str, MessageValue]):
     {'sender': 'Example sender'}
 
     A dictionary can be given to the constructor:
-    >>> m = Message('Example sender', {'key 1': 'value 1', 'key 2': 'value 2'})
+    >>> m = Message('Example sender',
+    ...             {'key 1': 'value 1', 'key 2': 'value 2'})
     >>> print(m)
     {'sender': 'Example sender', 'key 1': 'value 1', 'key 2': 'value 2'}
 
+    A 'sender' set in the initial dictionary is overwritten by the explicitly
+    given sender in the first argument:
+    >>> m = Message('Example sender',
+    ...             {'sender': 'Original sender', 'key': 'value'})
+    >>> print(m)
+    {'sender': 'Example sender', 'key': 'value'}
+
     Or the message can be modified after construction:
     >>> m = Message('Example sender', {'key 1': 'value 1'})
     >>> m['key 2'] = 'value 2'
     >>> print(m)
     {'sender': 'Example sender', 'key 1': 'value 1', 'key 2': 'value 2'}
+
+    The 'sender' key can be overwritten, but this should only be done in
+    exceptional cases:
+    >>> m = Message('Example sender', {'key': 'value'})
+    >>> m['sender'] = 'New sender'
+    >>> print(m)
+    {'sender': 'New sender', 'key': 'value'}
     """
 
     def __init__(self, sender: str,
@@ -168,18 +183,14 @@ class Message(Dict[str, MessageValue]):
         >>> m = Message('Example sender', {'key 1': 'value 1'})
         >>> print(m)
         {'sender': 'Example sender', 'key 1': 'value 1'}
-
-        The sender can be overwritten by the key-value pairs:
-        >>> m = Message('Example sender', {'sender': 'Another sender'})
-        >>> print(m)
-        {'sender': 'Another sender'}
         """
         if not isinstance(sender, str):
             raise TypeError(f"'{sender}' is not a valid sender name"
                             " (not a string).")
-        self['sender'] = sender
+        self['sender'] = ''
         if init is not None:
             self.update(init)
+        self['sender'] = sender
 
     @staticmethod
     def check_value(value: MessageValue) -> bool:
index 9928d18708fd8e8d4885665e56c931312bbf3e9d..5834a83086a97d5bc3dd5ee796c7116a3a049ba7 100644 (file)
@@ -72,15 +72,7 @@ class Log(BasePlugin):
     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'}
+    data must contain ['filter'] properties
     Configuration for 'Test Log' is not valid.
 
     The "filter" key has to contain a list of message templates, i.e.,
@@ -88,13 +80,7 @@ class Log(BasePlugin):
     >>> 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
+    data.filter[0] must be object
     Configuration for 'Test Log' is not valid.
     """
 
@@ -157,15 +143,7 @@ class Init(BasePlugin):
     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'}
+    data must contain ['messages'] properties
     Configuration for 'Test Init' is not valid.
 
     The "messages" key has to contain a list of (partial) messages, i.e.,
@@ -173,13 +151,7 @@ class Init(BasePlugin):
     >>> 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
+    data.messages[0] must be object
     Configuration for 'Test Init' is not valid.
     """
 
@@ -292,11 +264,16 @@ 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.
+    configuration key "to" a (partial) message. The "translate"
+    configuration key contains pairs of message keys, where the "from"
+    message key is translated to the "to" message key if present in the
+    message.
+
+    All messages matching the "from" template are received by the Alias
+    instance and a message translated by adding the keys and values of the
+    "to" message and the translated key-value pairs according to
+    "translate" is sent. Keys that are not "sender" and not modified by
+    "to" or "translate" 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
@@ -317,41 +294,16 @@ class Alias(BasePlugin):
     test(): {'sender': 'test()', 'id': 42,
              'content': 'Test Message', 'old': 'content'}
     test(): {'sender': 'Test Alias', 'id': 'translated',
-             'content': 'Test Message', 'new': 'content'}
+             'content': 'Test Message', 'old': 'content', 'new': 'content'}
     test(): {'sender': 'test()', 'id': 42,
              'content': 'Second Message', 'old': 'content'}
     test(): {'sender': 'Test Alias', 'id': 'translated',
-             'content': 'Second Message', 'new': 'content'}
+             'content': 'Second Message', 'old': 'content', 'new': 'content'}
 
-    The "from" and "to" keys are required:
+    The "from" key is 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'},
-                        'translate': {'items': {'properties': {'from': {'type': 'string'},
-                                                               'to': {'type': 'string'}},
-                                                'type': 'object'},
-                                      'type': 'array'}},
-         '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'},
-                        'translate': {'items': {'properties': {'from': {'type': 'string'},
-                                                               'to': {'type': 'string'}},
-                                                'type': 'object'},
-                                      'type': 'array'}},
-         'required': ['from', 'to']}
-    <BLANKLINE>
-    On instance:
-        {'plugin': 'Alias'}
+    data must contain ['from'] properties
     Configuration for 'Test Alias' is not valid.
 
     The "from" key has to contain a message template and the "to" key a
@@ -360,52 +312,44 @@ class Alias(BasePlugin):
     ...     {"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
+    data.from must be object
+    Configuration for 'Test Alias' is not valid.
+    >>> asyncio.run(controlpi.test(
+    ...     {"Test Alias": {"plugin": "Alias",
+    ...                     "from": {"id": {"const": 42}},
+    ...                     "to": 42}}, []))
+    data.to must be object
     Configuration for 'Test Alias' is not valid.
     """
 
     CONF_SCHEMA = {'properties': {'from': {'type': 'object'},
                                   'to': {'type': 'object'},
                                   'translate': {'type': 'array',
-                                                'items': {'type': 'object',
-                                                          'properties': {'from': {'type': 'string'},
-                                                                         'to': {'type': 'string'}}}}},
-                   'required': ['from', 'to']}
+                                                'items':
+                                                {'type': 'object',
+                                                 'properties':
+                                                 {'from':
+                                                  {'type': 'string'},
+                                                  'to':
+                                                  {'type': 'string'}}}}},
+                   'required': ['from']}
     """Schema for Alias plugin configuration.
 
     Required configuration keys:
 
     - 'from': template of messages to be translated.
+
+    Optional configuration keys:
+
     - 'to': translated message to be sent.
+    - 'translate': array of pairs of keys to be translated.
     """
 
-    async def alias(self, message: Message) -> None:
-        """Translate and send message."""
-        alias_message = Message(self.name)
-        alias_message.update(self.conf['to'])
-        for key in message:
-            if key != 'sender' and key not in self.conf['from']:
-                if key in self._translate:
-                    alias_message[self._translate[key]] = message[key]
-                else:
-                    alias_message[key] = message[key]
-        await self.bus.send(alias_message)
-
     def process_conf(self) -> None:
         """Register plugin as bus client."""
+        self._to = {}
+        if 'to' in self.conf:
+            self._to = self.conf['to']
         self._translate = {}
         if 'translate' in self.conf:
             for pair in self.conf['translate']:
@@ -415,6 +359,15 @@ class Alias(BasePlugin):
                           [self.conf['from']],
                           self.alias)
 
+    async def alias(self, message: Message) -> None:
+        """Translate and send message."""
+        alias_message = Message(self.name, message)
+        alias_message.update(self._to)
+        for key in self._translate:
+            if key in message:
+                alias_message[self._translate[key]] = message[key]
+        await self.bus.send(alias_message)
+
     async def run(self) -> None:
         """Run no code proactively."""
         pass