Source code for glompo.common.corebase

""" Abstract classes used to construct the :class:`BaseHunter` and :class:`BaseChecker` classes. """

import inspect
from abc import ABC, abstractmethod

__all__ = ("_CoreBase", "_CombiCore", "_OrCore", "_AndCore")

from typing import Generator, Iterable


class _CoreBase(ABC):
    """ Base on which :class:`.BaseHunter` and :class:`.BaseChecker` are built. """

    @property
    def last_result(self):
        """ The result of the last :meth:`__call__`. """
        return self._last_result

    @last_result.setter
    def last_result(self, val: bool):
        self._last_result = val

    def __init__(self):
        self._last_result = None

    @abstractmethod
    def __call__(self, *args, **kwargs):
        """ Main evaluation method to determine the result of the hunt / convergence. """

    def __iter__(self) -> Iterable['_CoreBase']:
        """ Provides a (flattened) iteration through all the bases which comprise the combined hunter/checker. """
        return iter([self])

    def __str__(self) -> str:
        """ Produces a string of the hunter/checker's name and configuration. """
        lst = ""
        signature = inspect.signature(self.__init__)
        for parm in signature.parameters:
            if parm in dir(self):
                lst += f"{parm}={getattr(self, parm)}, "
            else:
                lst += f"{parm}, "
        lst = lst[:-2]
        return f"{self.__class__.__name__}({lst})"

    def str_with_result(self) -> str:
        """ String representation of the object with its convergence result. """
        mess = str(self)
        mess += f" = {self._last_result}"
        return mess

    def reset(self):
        """ Clears previous evaluation result to avoid misleading printing.
        Resets :attr:`last_result` to :obj:`None`. Given that hunters and checkers are evaluated lazily, it is possible
        for misleading results to be returned by :meth:`str_with_result` indicating a hunt/check has been evaluated when
        it has not. Bases are thus reset before :meth:`__call__` to prevent this.
        """
        self._last_result = None


class _CombiCore(_CoreBase):
    """ Class to handle the AND/OR combination of two :class:`_CoreBase`\\s. """

    def __init__(self, base1: _CoreBase, base2: _CoreBase):
        super().__init__()
        for base in [base1, base2]:
            if not isinstance(base, _CoreBase):
                raise TypeError("_CombiCore can only be initialised with instances of _CoreBase subclasses.")
        self._base1 = base1
        self._base2 = base2
        self._index = -1

    def __call__(self, *args, **kwargs):
        self.reset()

    def _combi_string_maker(self, keyword: str):
        return f"[{self._base1} {keyword} \n{self._base2}]"

    def _combi_result_string_maker(self, keyword: str):
        return f"[{self._base1.str_with_result()} {keyword} \n" \
               f"{self._base2.str_with_result()}]"

    def reset(self):
        self._base1._last_result = None
        self._base1.reset()

        self._base2._last_result = None
        self._base2.reset()

    def __iter__(self) -> Generator[_CoreBase, None, None]:
        return self._bases()

    def _bases(self):
        """ Returns a generator which yields each of the bases which make up the _CombiCore. This is fully recursive
            but the returns are 'flat' (i.e. nesting is not preserved).
        """
        for base in (self._base1, self._base2):
            if isinstance(base, _CombiCore):
                for item in list(base._bases()):
                    yield item
            else:
                yield base


class _OrCore(_CombiCore):
    """ :class:`_CombiCore` which specifically handles OR combinations of :class:`._CoreBase`\\s. """

    def __call__(self, *args, **kwargs):
        super().__call__(*args, **kwargs)
        self._last_result = self._base1(*args, **kwargs) or self._base2(*args, **kwargs)
        return self._last_result

    def __str__(self):
        return self._combi_string_maker("|")

    def str_with_result(self) -> str:
        return self._combi_result_string_maker("|")


class _AndCore(_CombiCore):
    """ :class:`_CombiCore` which specifically handles AND combinations of :class:`._CoreBase`\\s. """

    def __call__(self, *args, **kwargs):
        super().__call__(*args, **kwargs)
        self._last_result = self._base1(*args, **kwargs) and self._base2(*args, **kwargs)
        return self._last_result

    def __str__(self):
        return self._combi_string_maker("&")

    def str_with_result(self) -> str:
        return self._combi_result_string_maker("&")