# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/07_util.ipynb.

# %% auto 0
__all__ = ['find_spec_or_none', 'find_last_spec', 'prep_kwargs', 'asset_attrs', 'asset_sub_pkg', 'asset_last', 'asset_name',
           'parse_asset', 'insys', 'find_asset_module', 'load_asset', 'try_loaded_attr', 'try_ducked_attr', 'unduck',
           'tryattr']

# %% ../nbs/07_util.ipynb 6
import io, sys
from functools import wraps

from contextlib import redirect_stderr, redirect_stdout

from importlib.abc import Loader
from importlib.util import (module_from_spec, find_spec)
from importlib.machinery import ModuleSpec

# %% ../nbs/07_util.ipynb 8
from types import ModuleType
from typing import Any, Union, Iterable

# %% ../nbs/07_util.ipynb 10
#| export

# %% ../nbs/07_util.ipynb 12
#| export

# %% ../nbs/07_util.ipynb 14
from .cons import DOT, DUCK_SPEC_ATTRS
from .type import P
from .errs import DuckException, DuckNameError, DuckImportError, DuckAttributeError, DuckModuleNotFoundError
from .grds import isnotiterstr, isduckspec, isduckmodule

# %% ../nbs/07_util.ipynb 17
@wraps(find_spec)
def find_spec_or_none(name: str, package: str | None = None) -> ModuleSpec | None:
    # NOTE: needed as find_spec will fail if stub is an attribute e.g. 
    # `pandas.core.arrays.categorical.CategoricalAccessor`
    try: return find_spec(name, package)
    except ModuleNotFoundError: return None

def find_last_spec(fullpath: str) -> tuple[ModuleSpec | None, str | None]:
    stubs = fullpath.split(DOT)
    while len(stubs) > 0:
        stub = DOT.join(stubs)
        spec = find_spec_or_none(stub)
        if spec: break
        stubs.pop()
    return spec, stub

# %% ../nbs/07_util.ipynb 19
def prep_kwargs(obj: object, *args: P.args, __attrs: tuple[str, ...] = DUCK_SPEC_ATTRS, **kwargs: P.kwargs) -> dict:
    objkws, argkws = dict(),  dict(zip(__attrs, args))
    for i, attr in enumerate(__attrs):
        objkws.setdefault(attr, getattr(obj, attr, None))
        argkws.setdefault(attr, objkws.get(attr))
        kwargs.setdefault(attr, argkws.get(attr))
    return kwargs

# %% ../nbs/07_util.ipynb 21
def asset_attrs(asset) -> tuple[str, ...]:
    *_, rest = asset.removesuffix(DOT).partition(DOT)
    return rest.split(DOT)

def asset_sub_pkg(asset) -> tuple[str, str]:
    name, _, attr = asset.removesuffix(DOT).rpartition(DOT)
    return name, attr

def asset_last(asset: str) -> str:
    *_, name = str(asset).removesuffix(DOT).rpartition(DOT)
    return name

def asset_name(asset, is_module: bool = False) -> str:
    if is_module or DOT not in asset: return asset
    name, _ = asset_sub_pkg(asset)
    return name

def parse_asset(asset: str, is_module: bool = False) -> tuple[str, str | None]:
    if is_module or DOT not in asset: return asset, None
    name, attr = asset_sub_pkg(asset)
    return name, attr

# %% ../nbs/07_util.ipynb 26
def insys(asset: str, is_module: bool = False) -> bool:
    return asset_name(asset, is_module) in sys.modules

def find_asset_module(asset: str, is_module: bool = False) -> ModuleType:
    name, attr = parse_asset(asset, is_module)
    if insys(asset, is_module): return sys.modules[name]
    
    spec: ModuleSpec | None = find_spec(name)
    module: ModuleType = module_from_spec(spec)
    loader: Loader = getattr(spec, 'loader', None)
    
    try: loader.exec_module(module)
    except NameError as e: ...
    return module

def load_asset(asset: str, is_module: bool = False):
    name, attr = parse_asset(asset, is_module)
    module = find_asset_module(asset, is_module)
    if is_module or attr is None: return module
    return getattr(module, attr)

def try_loaded_attr(asset: str, is_module: bool = False, hush: bool = False):
    if not hush: return load_asset(asset, is_module)
    with redirect_stderr(io.StringIO()), redirect_stdout(io.StringIO()):
        return load_asset(asset, is_module)

# %% ../nbs/07_util.ipynb 28
def try_ducked_attr(asset: str, ducked: Union['DuckModule', None] = None):
    if ducked is None: raise DuckImportError(asset)
    try:
        item = ducked
        for attr in asset_attrs(asset):
            item = getattr(item, attr)
        return item
    except NameError as e: raise DuckNameError(*e.args)
    except ModuleNotFoundError as e: raise DuckModuleNotFoundError(*e.args, e.name, e.path)
    except ImportError as e: raise DuckImportError(*e.args, e.name, e.path)
    except AttributeError as e: raise DuckAttributeError(*e.args, e.name, e.obj)
    except Exception as e: raise DuckException(*e.args)
    return item

# %% ../nbs/07_util.ipynb 30
def unduck(item: Any):
    try:
        if isduckspec(item): return item.duck
        elif callable(getattr(item, 'load', None)):
            return item.load()
    except Exception: ...
    return item

def _fromducks(var: Iterable):
    iterstr = isnotiterstr(var)
    if not iterstr: return var
    for i, e in enumerate(var): var[i] = unduck(e)
    return var

def _fromcall(var: Iterable):
    if not callable(var): return var
    try: return var()
    except: ...
    return var

# %% ../nbs/07_util.ipynb 32
def tryattr(
    asset: str, 
    is_module: bool = False, 
    ducked: Union['DuckModule', None] = None,
    default: Any = None,
    bases: tuple = (),
    attrs: dict = {},
    hush: bool = True,
    delayed: dict[str, bool] = dict(),
    **kwargs
) -> type:
    item, ierr, derr = None, False, False
    try: item = try_loaded_attr(asset, is_module, hush=hush)         
    except (NameError, ImportError, AttributeError): ierr = True    
    
    if isinstance(item, ModuleType) and is_module: return item
    elif not ierr: return item
    
    try: item = try_ducked_attr(asset, ducked)
    except (DuckException, DuckNameError, DuckImportError, DuckAttributeError, DuckModuleNotFoundError): derr = True
            
    attr = asset_last(asset)
    
    isducked = isduckmodule(item)

    itemname = getattr(item, '__name__', None)
    goodname, notfound = attr == itemname, attr != itemname
    maketype: bool = (isducked and notfound) or (ierr or derr) or default in (None, type)
    
    found = not isducked and goodname
    if found: return item
    
    
    for k, v in delayed.items():
        if not v and k in locals(): continue
        var = locals().get(k)
        var = _fromducks(var)
        var = _fromcall(var)
        try:
            match k:
                case 'attrs': attrs = var()
                case 'bases': bases = var()
                case 'default': default = var()
                case _: ...
        except: ...
    
    try:
        if maketype: 
            moddict = dict(__module__=globals().get('__module__', 'quac'))
            attrs.update(moddict)
            item = type(attr, bases, attrs)
            
        else: item = default
    
    except Exception: 
        item = default
    
    item = unduck(item)
    return item
