From 7394a9d4f625cdf2d29ca7780f0d09b9b0007e13 Mon Sep 17 00:00:00 2001 From: ajCameron Date: Mon, 11 Dec 2023 21:27:03 +0000 Subject: [PATCH] Add human-oriented describer methods to the Component Contract The following have been added to the Component interfaces: - Help - Description - Display Name - Status --- src/mewbot/api/v1.py | 79 +++++++++++++ src/mewbot/core/__init__.py | 223 ++++++++++++++++++++++++++++++++++++ tests/test_registry.py | 18 +++ 3 files changed, 320 insertions(+) diff --git a/src/mewbot/api/v1.py b/src/mewbot/api/v1.py index 75556d68..aecc6f7d 100644 --- a/src/mewbot/api/v1.py +++ b/src/mewbot/api/v1.py @@ -25,6 +25,7 @@ import abc import functools +import inspect from mewbot.api.registry import ComponentRegistry from mewbot.core import ( @@ -52,6 +53,32 @@ class Component(metaclass=ComponentRegistry): _id: str + def help(self) -> str: + """ + Returns a human-readable help string for this Component. + """ + return inspect.getsource(type(self)) + + def display_name(self) -> str: + """ + Return a human-readable display name for this Component. + """ + return type(self).__name__ + + def description(self) -> str: + """ + Return a human-readable description of this Component. + """ + if (cand_str := type(self).__doc__) is not None: + return cand_str + return f"{self.display_name()} has no doc string set." + + def status(self) -> str: + """ + Returns a human-readable status string for this Component. + """ + return f"Status for class {self.display_name()} is not defined." + def serialise(self) -> ConfigBlock: """ Create a Loader compatible configuration block for this Component. @@ -158,6 +185,32 @@ class Input: def __init__(self) -> None: self.queue = None + def help(self) -> str: + """ + Returns a human-readable help string for this Input. + """ + return inspect.getsource(type(self)) + + def display_name(self) -> str: + """ + Return a human-readable display name for this Input. + """ + return type(self).__name__ + + def description(self) -> str: + """ + Return a human-readable description of this Input. + """ + if (cand_str := type(self).__doc__) is not None: + return cand_str + return f"{self.display_name()} has no doc string set." + + def status(self) -> str: + """ + Returns a human-readable status string for this Input. + """ + return f"Status for class {self.display_name()} is not defined." + @staticmethod @abc.abstractmethod def produces_inputs() -> set[type[InputEvent]]: @@ -191,6 +244,32 @@ class Output: they can consume it. """ + def help(self) -> str: + """ + Returns a human-readable help string for this Output. + """ + return inspect.getsource(type(self)) + + def display_name(self) -> str: + """ + Return a human-readable display name for this Output. + """ + return type(self).__name__ + + def description(self) -> str: + """ + Return a human-readable description of this Output. + """ + if (cand_str := type(self).__doc__) is not None: + return cand_str + return f"{self.display_name()} has no doc string set." + + def status(self) -> str: + """ + Returns a human-readable status string for this Output. + """ + return f"Status for class {self.display_name()} is not defined." + @staticmethod @abc.abstractmethod def consumes_outputs() -> set[type[OutputEvent]]: diff --git a/src/mewbot/core/__init__.py b/src/mewbot/core/__init__.py index 9ab8fbff..32f00edb 100644 --- a/src/mewbot/core/__init__.py +++ b/src/mewbot/core/__init__.py @@ -17,6 +17,11 @@ - Base classes for InputEvent and OutputEvent, and type hints for the event queues. - Component helper, including an enum of component types and a mapping to the interfaces - TypedDict mapping to the YAML schema for components. + + + + + """ from __future__ import annotations @@ -27,6 +32,7 @@ import asyncio import dataclasses import enum +import inspect @dataclasses.dataclass @@ -38,8 +44,49 @@ class InputEvent: by :class:`~mewbot.core.Behaviour` This base event has no data or properties. Events must be immutable. + It does, however, have three diagnostic methods + - event_help - provides a help string + - event_display_name - provides a human-readable display name + - event_description - description of the event - may interface with logging + Unlike the other interfaces here, these have "event_" prepended. + This is because some InputEvents will have better call on the word "description" than us. + (E.g. rss - which has `description` as one of its fields). """ + def event_help(self) -> str: + """ + Returns a human-readable help string for this InputEvent. + + Practically, should be rarely used - included for completeness. + """ + return inspect.getsource(type(self)) + + def event_display_name(self) -> str: + """ + Return a human-readable display name for this InputEvent. + + Used for formatting logging statements. + """ + return type(self).__name__ + + def event_description(self) -> str: + """ + Return a human-readable description of this InputEvent. + + Eventually, may include details such as + - which IOConfig generated it + - when + """ + if (cand_str := type(self).__doc__) is not None: + return cand_str + return f"{self.event_display_name()} has no doc string set." + + def event_status(self) -> str: + """ + Return a human-readable status for this InputEvent. + """ + return f"InputEvent - {self.event_display_name()} has not had a particular status set." + @dataclasses.dataclass class OutputEvent: @@ -50,8 +97,44 @@ class OutputEvent: may emit output events via the :class:`~mewbot.core.EventQueue` This base event has no data or properties. Events must be immutable. + It does, however, have three diagnostic methods + - event_help - provides a help string + - event_display_name - provides a human-readable display name + - event_description - description of the event - may interface with logging + Unlike the other interfaces here, these have "event_" prepended. + This is because some OutputEvents will have better call on the word "description" than us. + (E.g. rss - which has `description` as one of its fields). """ + def event_help(self) -> str: + """ + Returns a human-readable help string for this OutputEvent. + + Practically, should be rarely used - included for completeness. + """ + return inspect.getsource(type(self)) + + def event_display_name(self) -> str: + """ + Return a human-readable display name for this OutputEvent. + """ + return type(self).__name__ + + def event_description(self) -> str: + """ + Return a human-readable description of this OutputEvent. + """ + if (cand_str := type(self).__doc__) is not None: + return cand_str + return f"{self.event_display_name()} has no doc string set." + + def event_status(self) -> str: + """ + Return a human-readable status for this OutputEvent. + """ + return f"OutputEvent - {self.event_display_name()} has not had a particular status set." + + InputQueue = asyncio.Queue[InputEvent] OutputQueue = asyncio.Queue[OutputEvent] @@ -80,6 +163,26 @@ class IOConfigInterface(Protocol): they are passed to the bot via the `get_inputs` and `get_outputs` methods. """ + def help(self) -> str: + """ + Returns a human-readable help string for this IOConfig. + """ + + def display_name(self) -> str: + """ + Return a human-readable display name for this IOConfig. + """ + + def description(self) -> str: + """ + Return a human-readable description for this IOConfig. + """ + + def status(self) -> str: + """ + Return a status string for this IOConfig. + """ + def get_inputs(self) -> Iterable[InputInterface]: """ Gets the Inputs that are used to read events from the service. @@ -110,6 +213,26 @@ class InputInterface(Protocol): into the bot's input event queue for processing. """ + def help(self) -> str: + """ + Returns a human-readable help string for this Input. + """ + + def display_name(self) -> str: + """ + Return a human-readable display name for this Input. + """ + + def description(self) -> str: + """ + Return a human-readable description for this Input. + """ + + def status(self) -> str: + """ + Return a status string for this Input. + """ + @staticmethod def produces_inputs() -> set[type[InputEvent]]: """List the types of Events this Input class could produce.""" @@ -141,6 +264,26 @@ class OutputInterface(Protocol): they can consume it. """ + def help(self) -> str: + """ + Returns a human-readable help string for this Output. + """ + + def display_name(self) -> str: + """ + Return a human-readable display name for this Output. + """ + + def description(self) -> str: + """ + Return a human-readable description for this Output. + """ + + def status(self) -> str: + """ + Return a status string for this Output. + """ + @staticmethod def consumes_outputs() -> set[type[OutputEvent]]: """Defines the types of Event that this Output class can send.""" @@ -165,6 +308,26 @@ class TriggerInterface(Protocol): Filtering behaviours is the role of the Condition Component. """ + def help(self) -> str: + """ + Returns a human-readable help string for this Trigger. + """ + + def display_name(self) -> str: + """ + Return a human-readable display name for this Trigger. + """ + + def description(self) -> str: + """ + Return a human-readable description for this Trigger. + """ + + def status(self) -> str: + """ + Return a status string for this Trigger. + """ + @staticmethod def consumes_inputs() -> set[type[InputEvent]]: """ @@ -193,6 +356,26 @@ class ConditionInterface(Protocol): see all events. """ + def help(self) -> str: + """ + Returns a human-readable help string for this Condition. + """ + + def display_name(self) -> str: + """ + Return a human-readable display name for this Condition. + """ + + def description(self) -> str: + """ + Return a human-readable description for this Condition. + """ + + def status(self) -> str: + """ + Return a status string for this Condition. + """ + @staticmethod def consumes_inputs() -> set[type[InputEvent]]: """ @@ -217,6 +400,26 @@ class ActionInterface(Protocol): - Add data to the state, which will be available to the other actions in the behaviour """ + def help(self) -> str: + """ + Return a human-readable help string for this Action. + """ + + def display_name(self) -> str: + """ + Returns a human-readable display name for this Action. + """ + + def description(self) -> str: + """ + Return a human-readable description for this Action. + """ + + def status(self) -> str: + """ + Return a status string for this Action. + """ + @staticmethod def consumes_inputs() -> set[type[InputEvent]]: """ @@ -262,6 +465,26 @@ class BehaviourInterface(Protocol): order, which can read from or write to DataStores, and emit OutputEvents. """ + def help(self) -> str: + """ + Return a human-readable help string for this Behavior. + """ + + def display_name(self) -> str: + """ + Returns a human-readable display name for this Behavior. + """ + + def description(self) -> str: + """ + Return a human-readable description for this Behavior. + """ + + def status(self) -> str: + """ + Return a status string for this Behavior. + """ + def add(self, component: TriggerInterface | ConditionInterface | ActionInterface) -> None: """ Adds a component to the behaviour which is one or more of a Tigger, Condition, or Action. diff --git a/tests/test_registry.py b/tests/test_registry.py index bf532c49..ac02c834 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -67,6 +67,24 @@ class Condiment(abc.ABC, metaclass=ComponentRegistry): Private class for testing. """ + @abc.abstractmethod + def help(self) -> str: + """ + Human-readable help string for this Condition. + """ + + @abc.abstractmethod + def display_name(self) -> str: + """ + Human-readable help string for this Condition. + """ + + @abc.abstractmethod + def description(self) -> str: + """ + Human-readable help string for this Condition. + """ + @staticmethod @abc.abstractmethod def consumes_inputs() -> set[Type[InputEvent]]: