"""Custom exception types raised by different Finesse functions and class methods."""
from __future__ import annotations
import abc
from typing import Any
from .env import traceback_handler_instance
from finesse.utilities.text import get_close_matches, option_list
[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 ExternallyControlledException(FinesseException):
"""Raised when a parameter value is changed but there are other elements that are
controlling what the value is."""
pass
[docs]class ComponentNotConnected(FinesseException):
pass
[docs]class ParameterLocked(FinesseException):
pass
[docs]class NoCouplingError(FinesseException):
"""Raised when a coupling at a component is requested but does not exist."""
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):
pass
def _add_suggestions_to_msg(
self, target: Any, resolved_attrs: list[str], missing_name: str, msg: str
) -> str | None:
# prevent circular imports
from finesse.model import Model
from finesse.element import ModelElement
from finesse.components import Connector
options = set()
if isinstance(target, Model):
options |= set(
x for x in dir(target) if (not x[0] == "_" and x not in dir(Model))
)
else:
if isinstance(target, ModelElement):
options |= set(par.name for par in target.parameters)
if isinstance(target, Connector):
options |= set(port.name for port in target.ports)
if not len(options):
options |= set(dir(target))
close_matches = get_close_matches(missing_name, options)
if close_matches:
prefix = ".".join(resolved_attrs) + "."
suggestions = option_list(
close_matches, quotechar="'", sort=True, prefix=prefix
)
msg += f"\n\nDid you mean: {suggestions}?"
else:
msg += f"\n\nNo suggestions found for '{missing_name}'"
return msg
[docs]class ModelMissingAttributeError(ModelAttributeError):
"""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, target: Any, resolved_attrs: list[str], missing_name: str):
"""Generate an error for a missing model attribute, including suggestions of
similar existing model attributes.
Parameters
----------
model : Model
The model with the missing attribute
pieces : list[str]
List of strings resembling a model path
"""
path = ".".join([*resolved_attrs, missing_name])
if path.endswith("."):
super().__init__(f"'{path}' should not end with a '.'")
return
msg = self._add_suggestions_to_msg(
target, resolved_attrs, missing_name, msg=f"model has no attribute '{path}'"
)
super().__init__(msg)
[docs]class ModelClassAttributeError(ModelAttributeError):
"""Error indicating that a model path resolves to a class attribute.
E.g. `l1.P.__dict__` or `parse` will resolve, but no usecase exists for referencing
these class attributes in katscript.
"""
def __init__(self, target: Any, resolved_attrs: list[str], missing_name: str):
msg = self._add_suggestions_to_msg(
target,
resolved_attrs,
missing_name,
msg=f"Forbidden word '{missing_name}' in this context (python class attribute)",
)
FinesseException.__init__(self, 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