"""Custom exception types raised by different Finesse functions and class methods."""
import abc
from .env import traceback_handler_instance
[docs]class FinesseException(Exception):
    """The exception type which gets raised upon a Finesse failure.
    This identifies whether the current session is interactive or not, and consequently
    sets the level of verbosity. This can be overridden by calling
    :func:`~finesse.env.show_tracebacks` with ``True``.
    """
    def __init__(self, message, **kwargs):
        if not traceback_handler_instance().show_tb:
            head = "\t(use finesse.tb() to see the full traceback)\n"
        else:
            head = "\n"
        message = head + str(message)
        super().__init__(message, **kwargs)
    def _render_traceback_(self):
        """use custom traceback in IPython/Jupyter."""
        tb = traceback_handler_instance()
        tb.store_tb()
        return tb.get_stb() 
[docs]class ComponentNotConnected(FinesseException):
    pass 
[docs]class ParameterLocked(FinesseException):
    pass 
[docs]class NodeException(FinesseException):
    """Exception associated with :class:`.Node` related run-time errors.
    Objects of type `NodeException` store the error message as well as an optional
    reference to the node(s) which caused the exception to be raised.
    Parameters
    ----------
    message : str
        The error message.
    node : :class:`.Node`, optional
        A reference to the offending node(s), defaults to `None`. This can be a single
        node or a sequence of nodes.
    """
    def __init__(self, message, node=None):
        super().__init__(message)
        self.__node = node
    @property
    def node(self):
        """The node(s) responsible for raising this exception instance.
        :getter: Returns the node(s) (either a single :class:`.Node` object
                 or a sequence of these objects) responsible for the exception
                 (read-only).
        """
        return self.__node 
[docs]class BeamTraceException(FinesseException):
    pass 
[docs]class ConvergenceException(FinesseException):
    """Indicates an algorithm has failed to converge to some requested tolerance."""
    pass 
[docs]class TotalReflectionError(FinesseException):
    """Exception indicating total reflection of a beam at a component when performing
    beam tracing.
    Parameters
    ----------
    message : str
        The error message.
    from_node, to_node : :class:`.Node`
        References to the offending source and target nodes, respectively.
    """
    def __init__(self, message, from_node=None, to_node=None):
        super().__init__(message)
        self.__from_node = from_node
        self.__to_node = to_node
    @property
    def coupling(self):
        """The tuple of (from, to) nodes responsible for the total reflection error.
        :getter: Returns the nodes responsible for the exception (read-only).
        """
        return (self.__from_node, self.__to_node) 
[docs]class ModelAttributeError(FinesseException):
    """Error indicating a model path was not found.
    Model paths can be e.g. `l1.P` or `s1.p1.o`.
    This exists mainly so it can be caught by the parser.
    """
    def __init__(self, model, pieces):
        from spellchecker import SpellChecker
        self.path = ".".join(pieces)
        if self.path.endswith("."):
            msg = f"'{self.path}' should not end with a '.'"
        else:
            msg = f"model has no attribute '{self.path}'"
            # Try and guess some plausible options
            curr = model
            for i, key in enumerate(pieces):
                if hasattr(curr, key):
                    curr = getattr(curr, key)
                else:
                    break
            correct = ".".join(pieces[:i])
            if len(correct) > 0:
                correct += "."
            spell = SpellChecker(language=None, case_sensitive=True)
            spell.word_frequency.load_words(dir(curr))
            candidates = spell.candidates(key)
            if candidates and key not in candidates:
                # Ensure suggestions are sorted alphabetically.
                suggestions = sorted([correct + option for option in candidates])
                msg += f"\n\nDid you mean: {', '.join(suggestions)}?"
            else:
                msg += f"\n\nNo suggestions found for '{key}'"
        super().__init__(msg) 
[docs]class ModelParameterDefaultValueError(FinesseException):
    """Error indicating a model element has no default model parameter.
    Some model parameters have defaults, such that they can be referenced in kat script
    using e.g. `myvar` instead of `myvar.value`. This error indicates a model element
    without such a default was referenced directly.
    """
    def __init__(self, element):
        super().__init__(
            f"{repr(element.name)} cannot be referenced because type "
            f"{repr(element.__class__.__name__)} has no default model parameter"
        ) 
[docs]class ModelParameterSelfReferenceError(FinesseException):
    """Error indicating a model parameter cannot be set to refer to itself."""
    def __init__(self, value, parameter):
        super().__init__(
            f"cannot set {parameter.full_name} to self-referencing value {value}"
        )
        self.value = value
        self.parameter = parameter 
class _empty:
    """Marker object for ContextualArgumentError.empty."""
[docs]class ContextualArgumentError(FinesseException, metaclass=abc.ABCMeta):
    """An argument error with additional context.
    This allows Finesse objects to provide additional information to the user when
    invalid values are passed to functions and methods.
    """
    empty = _empty 
[docs]class ContextualValueError(ContextualArgumentError):
    """A value error with additional information about value(s) that caused an error."""
[docs]    def __init__(self, params, extra_info=None):
        self.params = params
        self.extra_info = extra_info
        super().__init__(self.message()) 
[docs]    def message(self):
        from .utilities import ngettext, option_list
        pathstrs = option_list(self.params, final_sep="and", quotechar="'")
        problem = ngettext(
            len(self.params), "invalid value", "invalid values", sub=False
        )
        # Only print the values if there aren't empty ones.
        if any([v == self.empty for v in self.params.values()]):
            valuestrs = ""
        else:
            valuestrs = option_list(
                [repr(value) for value in self.params.values()], final_sep="and"
            )
            valuestrs = f" {valuestrs}"
        extra = f" ({self.extra_info})" if self.extra_info else ""
        return f"{pathstrs}: {problem}{valuestrs}{extra}"  
[docs]class ContextualTypeError(ContextualArgumentError):
    """A type error with additional information about the available types."""
[docs]    def __init__(self, param, value, allowed_types=None):
        self.param = param
        self.value = value
        self.allowed_types = allowed_types
        super().__init__(self.message()) 
[docs]    def message(self):
        from .utilities import option_list
        if self.allowed_types:
            allowedtypes = [t.__name__ for t in self.allowed_types]
            allowedstr = option_list(allowedtypes, quotechar="'")
            gotstr = f"'{type(self.value).__name__}'"
            problem = f" (expected {allowedstr}, got {gotstr})"
        else:
            problem = ""
        return f"{self.param}: invalid type{problem}"  
[docs]class NoLinearEquations(FinesseException):
    """Thrown when a simulation has no linear equations to solve."""
    pass