/ src / archan / plugins / __init__.py
__init__.py
  1  """Plugins submodule."""
  2  
  3  from __future__ import annotations
  4  
  5  from collections import namedtuple
  6  from typing import TYPE_CHECKING, Any, Sequence
  7  
  8  from archan.enums import ResultCode
  9  from archan.logging import Logger
 10  from archan.printing import PrintableArgumentMixin, PrintableNameMixin, PrintablePluginMixin
 11  
 12  if TYPE_CHECKING:
 13      from archan.dsm import DesignStructureMatrix, DomainMappingMatrix, MultipleDomainMatrix
 14  
 15  logger = Logger.get_logger(__name__)
 16  
 17  
 18  class Argument(PrintableArgumentMixin):
 19      """Placeholder for name, class, description and default value."""
 20  
 21      def __init__(self, name: str, cls: type, description: str, default: Any | None = None):
 22          """Initialization method.
 23  
 24          Parameters:
 25              name: Name of the argument.
 26              cls: Type of the argument.
 27              description: Description of the argument.
 28              default: Default value for the argument.
 29          """
 30          self.name = name
 31          self.cls = cls
 32          self.description = description
 33          self.default = default
 34  
 35      def __str__(self):
 36          return f"  {self.name} ({self.cls}, default {self.default}): {self.description}"
 37  
 38  
 39  # TODO: also add some "expect" attribute to describe the expected data format
 40  class Checker(PrintableNameMixin, PrintablePluginMixin):
 41      """Checker class.
 42  
 43      An instance of Checker implements a check method that analyzes an instance
 44      of DSM/DMM/MDM and return a true or false value, with optional message.
 45      """
 46  
 47      identifier = ""
 48      name = ""
 49      description = ""
 50      hint = ""
 51      argument_list: Sequence[Argument] = ()
 52  
 53      Code = ResultCode
 54  
 55      def __init__(
 56          self,
 57          name: str | None = None,
 58          description: str | None = None,
 59          hint: str | None = None,
 60          allow_failure: bool = False,  # noqa: FBT001, FBT002
 61          passes: Any | None = None,
 62          arguments: dict | None = None,
 63      ):
 64          """Initialization method.
 65  
 66          Parameters:
 67              name: The checker name.
 68              description: The checker description.
 69              hint: Hint provided for failures.
 70              allow_failure: Still pass if failed or not.
 71              passes: Boolean.
 72              arguments: Arguments passed to the check method when run.
 73          """
 74          if name:
 75              self.name = name
 76          if description:
 77              self.description = description
 78          if hint:
 79              self.hint = hint
 80  
 81          self.allow_failure = allow_failure
 82          self.passes = passes
 83          self.arguments = arguments or {}
 84          self.result = None
 85  
 86      def check(
 87          self,
 88          dsm: DesignStructureMatrix | MultipleDomainMatrix | DomainMappingMatrix,
 89          **kwargs: Any,
 90      ) -> tuple[Any, str]:
 91          """Check the data and return a result.
 92  
 93          Parameters:
 94              dsm: DSM/DMM/MDM instance to check.
 95              **kwargs: Additional arguments.
 96  
 97          Returns:
 98              result: Checker constant or object with a ``__bool__`` method.
 99              message: Optional messages.
100          """
101          raise NotImplementedError
102  
103      def run(self, data: DesignStructureMatrix | MultipleDomainMatrix | DomainMappingMatrix) -> None:
104          """Run the check method and format the result for analysis.
105  
106          Parameters:
107              data: DSM/DMM/MDM instance to check.
108          """
109          result_type = namedtuple("Result", "code messages")  # type: ignore[name-match]  # noqa: PYI024
110  
111          if self.passes is True:
112              result = result_type(Checker.Code.PASSED, "")
113          elif self.passes is False:
114              result = (
115                  result_type(Checker.Code.IGNORED, "") if self.allow_failure else result_type(Checker.Code.FAILED, "")
116              )
117          else:
118              try:
119                  result = self.check(data, **self.arguments)  # type: ignore[assignment]
120              except NotImplementedError:
121                  result = result_type(Checker.Code.NOT_IMPLEMENTED, "")
122              else:
123                  messages = ""
124                  if isinstance(result, tuple):
125                      result, messages = result
126  
127                  if result not in Checker.Code:
128                      result = Checker.Code.PASSED if bool(result) else Checker.Code.FAILED  # type: ignore[assignment]
129  
130                  if result == Checker.Code.FAILED and self.allow_failure:
131                      result = Checker.Code.IGNORED  # type: ignore[assignment]
132  
133                  result = result_type(result, messages)
134          self.result = result  # type: ignore[assignment]
135  
136  
137  class Provider(PrintableNameMixin, PrintablePluginMixin):
138      """Provider class.
139  
140      An instance of provider implements a get_data method that returns an
141      instance of DSM/DMM/MDM to be checked by an instance of Checker.
142      """
143  
144      identifier = ""
145      name = ""
146      description = ""
147      argument_list: tuple[Argument, ...] = ()
148  
149      def __init__(
150          self,
151          name: str | None = None,
152          description: str | None = None,
153          arguments: dict | None = None,
154      ) -> None:
155          """Initialization method.
156  
157          Parameters:
158              name: The provider name.
159              description: The provider description.
160              arguments: Arguments that will be used for `get_data` method.
161          """
162          if name:
163              self.name = name
164          if description:
165              self.description = description
166  
167          self.arguments = arguments or {}
168          self.data = None
169  
170      def get_data(self, **kwargs: Any) -> Any:
171          """Abstract method. Return instance of DSM/DMM/MDM.
172  
173          Parameters:
174              **kwargs: Keyword arguments.
175  
176          Raises:
177              NotImplementedError: This method must be implemented in subclasses.
178          """
179          raise NotImplementedError
180  
181      def run(self) -> None:
182          """Run the get_data method with run arguments, store the result."""
183          self.data = self.get_data(**self.arguments)