from __future__ import annotations
import functools
import logging
import os
import re
import types
from typing import Any, Callable, Dict, Optional, Type, Union
from logging import Logger
from hades.common.util import qualified_name
from hades.common.exc import SetupError
logger = logging.getLogger(__name__)
option_name_regex = re.compile(r'\A[A-Z][A-Z0-9_]*\Z', re.ASCII)
[docs]def is_option_name(name: str) -> bool:
    """
    Check if a given object is a valid option name.
    Valid option names are restricted to ASCII, start with an uppercase letter
    followed by uppercase letters (A-Z), digits (0-9) or the underscore (_).
    :param name: Name
    :return: True, if name is string and a valid option name, False otherwise
    """
    return bool(isinstance(name, str) and option_name_regex.match(name)) 
[docs]def option_reference(option: Union[Type[Option], str]):
    option = coerce(option)
    return ":hades:option:`{}`".format(option) 
# noinspection PyUnresolvedReferences
[docs]class Option(metaclass=OptionMeta, abstract=True):
    has_default = False
    required = False
    default: Any
    type: Optional[Type] = None
    runtime_check: Any = None
    static_check: Any = None 
[docs]class ConfigError(SetupError):
    """Base class for all config related errors."""
    exit_code = os.EX_CONFIG
    def __init__(self, *a, **kw):
        self.logger = kw.get("logger", logger)
        super().__init__(*a, **kw) 
[docs]class ConfigOptionError(ConfigError):
    """Base class for errors related to processing a specific option"""
    def __init__(self, *args, option: str, **kwargs):
        super().__init__(*args, **kwargs)
        self.option = option
[docs]    def report_error(self, fallback_logger: Logger) -> None:
        logger = self.logger or fallback_logger
        logger.critical(
            "Configuration error with option %s: %s", self.option, self
        )  
[docs]class MissingOptionError(ConfigOptionError):
    """Indicates that a required option is missing""" 
[docs]class OptionCheckError(ConfigOptionError):
    """Indicates that an option check failed""" 
[docs]def coerce(value: Union[Type[Option], str]) -> str:
    if isinstance(value, type) and issubclass(value, Option):
        return value.__name__
    elif isinstance(value, str):
        return value
    else:
        raise TypeError(
            f"value must be a string or an Option class, not {value!r}"
        ) 
[docs]class OptionDescriptor:
[docs]    @classmethod
    def decorate(cls: Type[OptionDescriptor], f):
        """
        Convert regular functions into an :class:`OptionDescriptor`.
        The function will be called with the option as its first argument.
        Functions are automatically decorated with :class:`classmethod`, if they
        are not already an instance of :class:`classmethod` or
        :class:`staticmethod`.
        :param f: The function
        """
        # Ensure that we have a static or class method
        if not isinstance(f, (classmethod, staticmethod)):
            m = classmethod(f)
        else:
            m = f  # type: ignore
        # noinspection PyPep8Naming
        @functools.wraps(f, updated=())
        class Wrapper(cls):  # type: ignore  # see #mypy/5865
            """Descriptor, that binds the given function in addition to an
            option"""
            def __init__(self, *args, **kwargs):
                super().__init__(*args, **kwargs)
                self.bound = None
            def __call__(self, *args, **kwargs):
                return self.bound(*args, **kwargs)
            def __get__(self, instance, owner):
                if self.option is None:
                    self.bound = m.__get__(instance, owner)
                return super().__get__(instance, owner)
        # Unfortunately `functools.wraps` is not sufficient, as `staticmethod`
        # and `classmethod` are not propagating the original `__doc__` until
        # Python 3.10.
        # See https://bugs.python.org/issue43682#msg390496.
        Wrapper.__doc__ = m.__func__.__doc__
        return Wrapper() 
    def __init__(self):
        self.option = None
    def __get__(self, instance, owner):
        if self.option is None:
            self.option = owner
        return self 
[docs]class Check(OptionDescriptor):
    """Base class for descriptors, that check the value of options"""
    def __call__(self, config, value):
        """Check the ``value`` of an option given ``config``.
        :param config: The fully expanded config
        :param value: The value of the Option
        :raises OptionCheckError: if the value of the option is illegal
        """
        raise NotImplementedError() 
[docs]class Compute(OptionDescriptor):
    """Base class for descriptors, that compute the value of options."""
    def __call__(self, config):
        """Compute the value of the option using ``config``.
        :param config: An potentially not fully expanded config object
        :raises OptionCheckError: if the value can't be computed
        """
        raise NotImplementedError()