# -*- coding: utf-8 -*-
"""This module implements some exception classes and related features."""
# import...
# ...from standard-library
import enum
import importlib
import types
# ...from HydPy
from hydpy.core import objecttools
from hydpy.core.typingtools import *
[docs]
class HydPyDeprecationWarning(DeprecationWarning):
    """Warning for deprecated *HydPy* features.""" 
[docs]
class AttributeNotReady(RuntimeError):
    """The attribute is principally defined but so far unprepared.""" 
[docs]
class AttributeNotReadyWarning(Warning):
    """The attribute is principally defined but so far unprepared.""" 
[docs]
def attrready(obj: Any, name: str) -> bool:
    """Return |False| when trying the access the attribute of the given object
    results in an |AttributeNotReady| error and otherwise return |True|.
    In *HydPy*, some properties raise an |AttributeNotReady| error when one
    tries to access them before they are correctly set.  You can use method
    |attrready| to find out the current state of such properties without
    doing the related exception handling on your own:
    >>> from hydpy import attrready
    >>> from hydpy.core.parametertools import Parameter
    >>> class Par(Parameter):
    ...     NDIM, TYPE = 1, float
    >>> par = Par(None)
    >>> attrready(par, "NDIM")
    True
    >>> attrready(par, "ndim")
    Traceback (most recent call last):
    ...
    AttributeError: 'Par' object has no attribute 'ndim'
    >>> attrready(par, "shape")
    False
    >>> par.shape = 2
    >>> attrready(par, "shape")
    True
    """
    try:
        getattr(obj, name)
    except AttributeNotReady:
        return False
    return True 
[docs]
def hasattr_(obj: Any, name: str) -> bool:
    """Return |True| or |False| whether the object has an attribute with the
    given name.
    In *HydPy*, some properties raise an |AttributeNotReady| error when one
    tries to access them before they are correctly set, which also happens
    when one applies function |hasattr| to find out if the related object
    handles the property at all.  Function |hasattr_| extends function
    |hasattr| by also catching |AttributeNotReady| errors:
    >>> from hydpy import hasattr_
    >>> from hydpy.core.parametertools import Parameter
    >>> class Par(Parameter):
    ...     NDIM, TYPE = 1, float
    >>> par = Par(None)
    >>> hasattr_(par, "NDIM")
    True
    >>> hasattr_(par, "ndim")
    False
    >>> hasattr_(par, "shape")
    True
    >>> par.shape = 2
    >>> attrready(par, "shape")
    True
    """
    try:
        return hasattr(obj, name)
    except AttributeNotReady:
        return True 
class _Enum(enum.Enum):
    GETATTR_NO_DEFAULT = 1
@overload
def getattr_(obj: Any, name: str) -> Any: ...
@overload
def getattr_(obj: Any, name: str, default: None) -> Optional[Any]: ...
@overload
def getattr_(obj: Any, name: str, default: T) -> T: ...
@overload
def getattr_(obj: Any, name: str, *, type_: type[T]) -> T: ...
@overload
def getattr_(obj: Any, name: str, default: None, type_: type[T]) -> Optional[T]: ...
@overload
def getattr_(obj: Any, name: str, default: T1, type_: type[T2]) -> Union[T1, T2]: ...
[docs]
def getattr_(
    obj: Any,
    name: str,
    default: Union[T, _Enum] = _Enum.GETATTR_NO_DEFAULT,
    type_: Optional[type[T]] = None,
) -> Any:
    """Return the attribute with the given name or, if it does not exist,
    the default value, if available.
    In *HydPy*, some properties raise an |AttributeNotReady| error when one
    tries to access them before they are correctly set, which also happens
    when one applies function |getattr| with default values.  Function
    |getattr_| extends function |getattr| by also returning the default
    value when an |AttributeNotReady| error occurs:
    >>> from hydpy import getattr_
    >>> from hydpy.core.parametertools import Parameter
    >>> class Par(Parameter):
    ...     NDIM, TYPE = 1, float
    >>> par = Par(None)
    >>> getattr_(par, "NDIM")
    1
    >>> getattr_(par, "NDIM", 2)
    1
    >>> getattr_(par, "ndim")
    Traceback (most recent call last):
    ...
    AttributeError: 'Par' object has no attribute 'ndim'
    >>> getattr_(par, "ndim", 2)
    2
    >>> getattr_(par, "shape")
    Traceback (most recent call last):
    ...
    hydpy.core.exceptiontools.AttributeNotReady: Shape information for \
variable `par` can only be retrieved after it has been defined.
    >>> getattr_(par, "shape", (4,))
    (4,)
    >>> par.shape = 2
    >>> getattr_(par, "shape")
    (2,)
    >>> getattr_(par, "shape", (4,))
    (2,)
    Function |getattr_| allows to specify the expected return type and raises
    an |AssertionError| if necessary:
    >>> getattr_(par, "NDIM", type_=int)
    1
    >>> getattr_(par, "NDIM", type_=str)
    Traceback (most recent call last):
    ...
    AssertionError: returned type: int, allowed type: str
    Additionally, it infers the expected return type from the default argument
    automatically:
    >>> getattr_(par, "NDIM", "wrong")
    Traceback (most recent call last):
    ...
    AssertionError: returned type: int, allowed type(s): str
    If the attribute type differs from the default value type, you need to define
    the attribute type explicitly:
    >>> getattr_(par, "NDIM", "wrong", int)
    1
    """
    if default is _Enum.GETATTR_NO_DEFAULT:
        value = getattr(obj, name)
        if type_ is not None and not isinstance(value, type_):
            raise AssertionError(
                f"returned type: {type(value).__name__}, "
                f"allowed type: {type_.__name__}"
            )
        return value
    try:
        value = getattr(obj, name, default)
    except AttributeNotReady:
        return default
    types_ = ((type_,) if type_ else ()) + ((type(default),) if default else ())
    if types_ and not isinstance(value, types_):
        raise AssertionError(
            f"returned type: {type(value).__name__}, allowed type(s): "
            f"{objecttools.enumeration(type_.__name__ for type_ in types_)}"
        )
    return value 
[docs]
class OptionalModuleNotAvailable(ImportError):
    """A `HydPy` function requiring an optional module is called, but this
    module is not available.""" 
[docs]
class OptionalImport(types.ModuleType):
    """Imports the first found module "lazily".
    >>> from hydpy.core.exceptiontools import OptionalImport
    >>> numpy = OptionalImport(
    ...     "numpy", ["numpie", "numpy", "os"], locals())
    >>> numpy.nan
    nan
    If no module could be imported at all, |OptionalImport| returns a
    dummy object which raises a |OptionalModuleNotAvailable| each time
    a one tries to access a member of the original module.
    >>> numpie = OptionalImport("numpie", ["numpie"], locals())
    >>> numpie.nan
    Traceback (most recent call last):
    ...
    hydpy.core.exceptiontools.OptionalModuleNotAvailable: HydPy could not \
load one of the following modules: `numpie`.  These modules are no general \
requirement but installing at least one of them is necessary for some \
specific functionalities.
    Note the very special case that |OptionalImport| raises a plain
    |AttributeError| when asked for the attribute `__wrapped__` (to avoid
    trouble when applying function `wrapt` of module |inspect|):
    >>> numpie.__wrapped__
    Traceback (most recent call last):
    ...
    AttributeError
    """
    def __init__(
        self, name: str, modules: list[str], namespace: dict[str, Any]
    ) -> None:
        self._name = name
        self._modules = modules
        self._namespace = namespace
    def __getattr__(self, name: str) -> Any:
        if name == "__wrapped__":
            raise AttributeError
        module = None
        for modulename in self._modules:
            try:
                module = importlib.import_module(modulename)
                self._namespace[self._name] = module
                break
            except ImportError:
                pass
        if module:
            return getattr(module, name)
        raise OptionalModuleNotAvailable(
            f"HydPy could not load one of the following modules: "
            f"`{objecttools.enumeration(self._modules)}`.  These modules are "
            f"no general requirement but installing at least one of them "
            f"is necessary for some specific functionalities."
        )