# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/00_core.ipynb.

# %% auto 0
__all__ = ['Spec', 'matr', 'ImportedAttribute', 'iattr', 'matrs', 'iattrs', 'pmod']

# %% ../nbs/00_core.ipynb 5
from functools import wraps

# %% ../nbs/00_core.ipynb 7
from types import ModuleType
from typing import (Self, Type, Union, Optional, NamedTuple, ClassVar, Iterable, )

# %% ../nbs/00_core.ipynb 9
from nchr import U1
from nlit import (
    ASSET, AREPR, IMPORTED, DEFAULT, MODULE, 
     __DOC__, __MODULE__, __QUALNAME__, __ANNOTATIONS__
)
from pdec import slotprops, Property
from itry import imod, T, Types, Namespace, ImportedAsset

# %% ../nbs/00_core.ipynb 11
_AREPR, _ASSET, _IMPORTED, _MODULE = f'{U1}{AREPR}', f'{U1}{ASSET}', f'{U1}{IMPORTED}', f'{U1}{MODULE}'

# %% ../nbs/00_core.ipynb 13
class Spec(NamedTuple):
    '''Specification information to import an asset.

    Parameters
    ----------
    name : str, optional
        The name of the module to import from, defaults to None.
        
    attr : str, optional
        The name of the attribute to import from the module, defaults to None.
        
    bases : Union[Types, type]
        Base classes for creating a new type if import fails and default is None.
        
    namespace : dict
        A namespace dictionary for creating a new type.
        
    default : type, optional
        A default value to return if import fails.
    '''
    name: Optional[str] = None
    attr: Optional[str] = None
    bases: Union[Types, type] = tuple()
    namespace: Namespace = dict()
    default: Optional[T] = None
    
    def asdict(self) -> dict:
        return self._asdict()
    
    def getvals(self, obj: object, __type: bool = False, **kwargs) -> dict:
        dct, res = self.asdict(), dict()
        itm = type(obj) if __type else obj
        for k, v in dct.items():
            val = kwargs.get(k, getattr(itm, k, v)) or v
            if val is not None and val:
                res[k] = val
            
        return res
    
    def setvals(self, obj: object, __type: bool = False, **kwargs):
        item = type(obj) if __type else obj
        kwargs.update(dict(__type = __type))
        vals = self.getvals(obj, **kwargs)
        for k, v in vals.items():
            setattr(item, k, v)
        return item

# %% ../nbs/00_core.ipynb 15
@slotprops(dict(
    asset = Property(ASSET, ImportedAsset, doc = 'The imported asset', gen = lambda self: self.__import__()),
    arepr = Property(AREPR, ImportedAsset, doc = 'String representation of the imported asset.', gen = lambda self: self.__asset_repr__()),
    imported = Property(IMPORTED, bool, False, 'Indicates if the import was successful.'),
))
class matr:
    '''A class representing a module's attribute (matr), imported if needed.

    Attributes
    ----------
    name : str
        The name of the module or attribute.
        
    attr : str, optional
        The specific attribute to import, if any.
        
    bases : tuple[type, ...]
        Base classes for creating new types if import fails.
        
    namespace : namespace | dict
        A namespace dictionary for creating new types.
        
    default : Any, optional
        Default value if the import fails.
        
    asset : Union[Type, FunctionType, BuiltinFunctionType, BuiltinMethodType, ModuleType], optional
        Property for the imported asset.
        
    arepr : str, optional
        Property for the string representation of the imported asset.
        
    imported : bool, default = False
        Property indicating if the import was successful.

    Methods
    -------
    io(ins: 'matr')
        Import an instance of matr class.
        
    ib(bases: Types)
        Import bases using the `io` method.
        
    __init__(self, name, attr, bases, namespace, default, eager, **kwargs)
        Initialize the matr instance.
        
    __import__(self)
        Import the asset using the `imod` method.
        
    __asset_repr__(self)
        Return the string representation of the asset.
        
    __call__(self, *args, **kwargs)
        Call the imported asset if callable.
        
    __repr__(self)
        Return the string representation of the matr instance.
        
    __getitem__(self, key)
        Get an item from the imported asset.
        
    __getattribute__(self, __name)
        Override getattribute to prioritize the asset's attributes.
    '''
    name: ClassVar[str]
    attr: ClassVar[Optional[str]] = None
    bases: ClassVar[Union[Types, type]] = tuple()
    namespace: ClassVar[Namespace] = dict()
    default: ClassVar[Optional[T]] = None
    
    asset: ImportedAsset
    arepr: Optional[str]
    imported: bool
    
    @classmethod
    def io(cls: Type[Self], ins: 'matr') -> ImportedAsset:
        '''Import instance of `matr` class'''
        if not isinstance(ins, cls): 
            return ins
        if not isinstance(ins, matr): 
            return ins
        return ins.__import__()
    
    @classmethod
    def ib(cls: Type[Self], bases: Types = tuple()) -> Types:
        '''Import bases using `io` method which is fault tolerant'''
        if not isinstance(bases, (list, tuple, Iterable)): 
            bases = (bases,)
        return tuple(cls.io(base) for base in bases)
    
    def __init__(
        self: Self, 
        name: str, 
        attr: Optional[str] = None, 
        bases: Types = tuple(), 
        namespace: Namespace = dict(), 
        default: Optional[T] = None, 
        eager: bool = False, 
        **kwargs
    ):
        spec = Spec(name, attr, bases, namespace, default)
        spec.setvals(self, __type = False, **kwargs)
        self._imported = False
        if eager:
            self.__import__()
            
    def __import__(self: Self) -> ImportedAsset:
        '''Import asset using `itry.imod` method which is fault tolerant. 
        
        If any of the classes in `self.bases` are `matr` instances,
        they will be imported first and then passed to `imod` as bases.
        '''
        if getattr(self, _ASSET, None) is not None:
            return self.asset
        
        if getattr(self, _IMPORTED, False):
            return self.asset
        
        asset = imod(self.name, self.attr, self.ib(self.bases), self.namespace, self.default)
        self._asset = asset

        
        if getattr(self, _ASSET, None) != getattr(self, DEFAULT, None): 
            self._imported = True
            
        return self.asset
        return self._asset
    
    def __asset_repr__(self: Self) -> Optional[str]:
        '''Return repr of asset if it exists, otherwise return None.'''
        if not hasattr(self, _AREPR): 
            return None
        
        if getattr(self, _AREPR, None) is None: 
            return repr(self._asset)
        
        return self._arepr
    
    def __call__(self: Self, *args, **kwargs) -> ImportedAsset:
        try:
            
            if callable(self.asset): 
                return self.asset(*args, **kwargs)
        except: 
            ...
        return self.asset
        
    def __repr__(self) -> str:
        return self.arepr or f'{self.name}({self.attr})'
    
    def __getitem__(self: Self, key: str) -> ImportedAsset:
        if hasattr(self, _ASSET): 
            return getattr(self._asset, key)
        
        return getattr(self, key, self.default)
    
    def __getattribute__(self: Self, __name: str, __recursed: bool = False):
        # prioritize self first
        try: 
            return super().__getattribute__(__name)
        except AttributeError as err:
            if __recursed:
                raise err
        # then try asset
        try:
            asset = object.__getattribute__(self, _ASSET)
            return object.__getattribute__(asset, __name)
        
        except AttributeError:
            ...
        # then back to self and raise error if not found
        return self.__getattribute__(__name, True)
        return super().__getattribute__(__name)
    
    
# Alias for `matr` class
@wraps(matr, updated=())
class ImportedAttribute(matr):
    '''An alias for the matr class.'''

@wraps(matr, updated=())
class iattr(matr):
    '''An alias for the matr class.'''

# %% ../nbs/00_core.ipynb 22
def matrs(
    name: str, 
    *attrs: str, 
    bases: Union[Types, type] = tuple(), 
    namespace: Namespace = dict(), 
    **kwargs
) -> tuple[matr, ...]:
    '''Try to import multiple attributes from a module, returning a tuple of results.

    Parameters
    ----------
    name : str
        The name of the module to import from.

    attrs : str
        Variable number of attribute names to import from the module.

    bases : Union[Tuple[type, ...], type]
        Base classes for creating new types if import fails.

    namespace : dict
        A namespace dictionary for creating new types.

    **kwargs
        Additional keyword arguments passed to imod.

    Returns
    -------
    Tuple[ImportedAttribute, ...]
        A tuple of the imported attributes or default values.
    '''
    return tuple(matr(name, attr, bases, namespace, **kwargs) for attr in attrs)


@wraps(matrs)
def iattrs(*args, **kwargs):
    return matrs(*args, **kwargs)

# %% ../nbs/00_core.ipynb 24
class pmod(ModuleType):
    '''A proxy for a module (pmod), which dynamically imports and 
    exposes attributes from the specified module.'''
    def __init__(self, module):
        super().__init__(module.__name__, module.__doc__)
        self._module = module

    def __getattr__(self, name: str):
        attr = getattr(self._module, name)
        for dund in (__DOC__, __MODULE__, __QUALNAME__, __ANNOTATIONS__):
            try: setattr(attr, dund, getattr(attr, dund, None))
            except Exception: ...
        return attr

    def __setattr__(self, name: str, value):
        if name == _MODULE:
            super().__setattr__(name, value)
        else:
            setattr(self._module, name, value)

    def __dir__(self):
        return dir(self._module)
        
    def names(self, withmagic: bool = False) -> list[str]:
        return [
            name for name in dir(self) 
            if (True if withmagic else not name.startswith(U1))
        ]
        
    def attrs(
        self: Self, 
        withmagic: bool = False
    ) -> dict:
        vals = {name: getattr(self, name) for name in self.names(withmagic)}
        return vals
    
    def populate(
        self: Self, 
        namespace: Optional[Namespace] = globals(),
        withmagic: bool = False
    ) -> Self:
        namespace = namespace or globals()
        namespace.update(self.attrs(withmagic=withmagic))
        return self
    
    
