# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/03_grds.ipynb.

# %% auto 0
__all__ = ['OriginTypes', 'OriginGuard', 'hashash', 'ishashable', 'hasorigin', 'isspecial', 'isnotspecial', 'allnotspecial',
           'getnotspecials', 'notspecialsorigins', 'isorigin', 'iscls', 'isstr', 'notcls', 'isnone', 'isbool', 'isdict',
           'isiter', 'isnan', 'istuple', 'notnone', 'isargs', 'istyping', 'isnotiterstr', 'isclstype', 'hasduck',
           'hasload', 'istype', 'isallsame', 'isalias']

# %% ../nbs/03_grds.ipynb 6
import io, math, inspect
from operator import eq, is_
from functools import wraps
from contextlib import redirect_stderr

# %% ../nbs/03_grds.ipynb 8
from types import FunctionType
from typing import (
    Any, Union, Callable, Iterable, TypeAlias, TypeGuard, get_args, get_origin, 
    Generic, Protocol, _SpecialForm, )

# %% ../nbs/03_grds.ipynb 10
#| export

# %% ../nbs/03_grds.ipynb 12
#| export

# %% ../nbs/03_grds.ipynb 14
from .cons import U1, U2, DUCK, LOAD, __HASH__, ALL_ATTRS
from .cons import NIL, NDIM, SHAPE, KEYS, VALUES, __K__, __V__, __N__, __RETANNOT__, __KVTYPES__
from .type import T, O
from .prot import Hashable, NonStrBytesIterable, Origin, Special, NotSpecial
from .prot import DuckSpecProtocol, DuckModuleProtocol, DuckTypeProtocol

# %% ../nbs/03_grds.ipynb 17
def hashash(o: object) -> TypeGuard[Hashable]:
    '''Check if the given object is hashable.'''
    return hasattr(o, __HASH__)

@wraps(hashash)
def ishashable(o: object) -> TypeGuard[Hashable]:
    return hashash(o)

def hasorigin(t: type) -> TypeGuard[Origin]:
    return bool(get_origin(t)) and hashash(t)

def isspecial(t: type) -> TypeGuard[Special]:
    return hasorigin(t) and isinstance(t, _SpecialForm) or t in (Generic, Protocol)

def isnotspecial(t: type) -> TypeGuard[NotSpecial]:
    return hasorigin(t) and not isspecial(t)

# %% ../nbs/03_grds.ipynb 19
def allnotspecial() -> tuple[NotSpecial, ...]:
    import typing
    return tuple(t for attr in dir(typing) if isnotspecial(t := getattr(typing, attr)))

def getnotspecials() -> Union[tuple[NotSpecial, ...]]:
    types: tuple[type, ...] = allnotspecial()
    return Union[types]

def notspecialsorigins() -> set[type]:
    return set([get_origin(o) for o in allnotspecial()])

# %% ../nbs/03_grds.ipynb 20
OriginTypes = Union[allnotspecial()]
OriginGuard: TypeAlias = TypeGuard[OriginTypes]

# %% ../nbs/03_grds.ipynb 21
def isorigin(t: type) -> OriginGuard:
    '''Check if the given type is one of the origin types defined in `OriginTypes`.'''
    return get_origin(t) in notspecialsorigins()

def ishashable(o: object) -> TypeGuard[Hashable]:
    '''Check if the given object is hashable.'''
    return hashash(o)

# %% ../nbs/03_grds.ipynb 23
def iscls(x) -> TypeGuard[type]:
    '''Check if `x` is a class.'''
    return inspect.isclass(x)

def isstr(x) -> TypeGuard[str]:
    '''Check if `x` is an `str`.'''
    return isinstance(x, str)

def notcls(x) -> TypeGuard[T]:
    '''Check if `x` is not a class.'''
    return not iscls(x)

def isnone(x) -> TypeGuard[None]:
    '''Check if `x` is `None`'''
    return x is None

def isbool(x) -> TypeGuard[bool]:
    '''Check if `x` is a `bool`'''
    return isinstance(x, bool)

def isdict(x) -> TypeGuard[dict]:
    '''Check if `x` is a `dict`'''
    return isinstance(x, dict)

def isiter(o: object) -> TypeGuard[Iterable]: 
    '''Check if the given object is an iterable.'''
    return isinstance(o, Iterable)

def isnan(o: object) -> TypeGuard[math.nan]:
    '''Check if the given object is `math.nan`.'''
    try: return math.isnan(o)
    except: return False

def _istype(x) -> TypeGuard[type]:
    '''Check if `x` is a `type`'''
    return isinstance(x, type)

def istuple(x) -> TypeGuard[tuple]:
    '''Check if `x` is a `tuple`'''
    return isinstance(x, tuple)

def notnone(x) -> TypeGuard[Any]:
    '''Check if `x` is not `None`'''
    return not isnone(x)

def isargs(o: object, t: T) -> TypeGuard[T]:
    '''Check if the given object is equal to or contained within the arguments of the type `t`.'''
    try: 
        args = get_args(t)
        # NOTE: len(args) and o == args makes sure empty tuple() is not True
        return any(((len(args) and o == args), o in args, o == t))
    except: return False

def istyping(o: object, t: T) -> TypeGuard[T]:
    '''Check if the given object is a typing construct that matches type `t`.'''
    if not isorigin(t): return False
    try: return isargs(o, t)
    except: return False
    
def isnotiterstr(o: object) -> TypeGuard[NonStrBytesIterable]:
    '''Check if the given object is an iterable but not a string or bytes.'''
    return isiter(o) and not isinstance(o, (str, bytes))

def isclstype(o: object) -> TypeGuard[type]:
    '''Check if the given object is a class type.'''
    return iscls(o) or isinstance(o, type)

# %% ../nbs/03_grds.ipynb 24
def hasduck(o: object) -> TypeGuard[T]:
    '''Check if the given object has the attribute `duck`.'''
    return hasattr(o, DUCK)

def hasload(o: object) -> TypeGuard[T]:
    '''Check if the given object has the attribute `load` and it is callable.'''
    return callable(getattr(o, LOAD, None))

# %% ../nbs/03_grds.ipynb 26
def istype(o: object, t: T, guards: Iterable[FunctionType] = tuple(), dropfn: Iterable[FunctionType] = tuple()) -> TypeGuard[T]:
    '''Check if the given object is of type `t`, using additional guard functions.

    Parameters
    ----------
    o : object
        The object to check.
        
    t : T
        The type to check against.
        
    guards : IterFunc, optional
        Additional guard functions to use in the check.
        
    dropfn : IterFunc, optional
        Functions to exclude from the check.

    Returns
    -------
    Guard
        True if the object is of type `t` or satisfies any of the guard functions, otherwise False.
        
    Notes
    -----
    The `guards` and `dropfn` parameters are used to filter the set of functions used in the check.
    
    The mandatory guard functions are `isinstance`, `issubclass`, `typs.istyping`, `typs.isargs`, 
        `operator.eq`, and `operator.is_`.
    '''
    funcs = (isinstance, issubclass, istyping, isargs, eq, is_,) + tuple(guards)
    funcs = (f for f in funcs if f not in dropfn)
    with redirect_stderr(io.StringIO()):
        for comp in funcs:
            try:
                if comp(o, t):
                    return True
            except: 
                continue
            
        if isnan(t): # checknan(o, t) ==> isnan(t) and isnan(o)
            if isnan(o): 
                return True
    # print(f'istype({o}, {t}), False')
    return False

# %% ../nbs/03_grds.ipynb 28
def isallsame(it: Iterable[T], dtype: T) -> TypeGuard[Iterable[T]]:
    '''Check if all elements in an iterable are of the same specified type.

    Parameters
    ----------
    it : IterType
        The iterable to check.
        
    dtype : T
        The type against which to check the elements of the iterable.

    Returns
    -------
    GuardIterType
        True if all elements are of the specified type, otherwise False.
    '''
    if not isiter(it): 
        return istype(it, dtype)
    
    for el in it:
        if isnotiterstr(el):
            # el is also iterable (but not like 'abc'), so recurse over the sub-iterable elements
            try:
                if not isallsame(el, dtype): 
                    return False
            except TypeError: 
                return False
            
        elif not istype(el, dtype):
            return False
    return True

# %% ../nbs/03_grds.ipynb 30
def isalias(
    obj: T,       # object to check
    types: tuple[type, ...], # types to check
    guards: Iterable[FunctionType] = tuple(),
    attrs: dict = dict(),
    hasattrs: Iterable[str] = tuple(),  # attributes to check
    allattrs: Iterable[str] = ALL_ATTRS, # attributes to check that values are all the same if object is iterable
    mapattrs: dict[str, Callable[[O, T], TypeGuard[T]]] = dict(),
) -> TypeGuard[Iterable[T]]:
    '''Check if an object matches a set of types and specific attributes.

    Parameters
    ----------
    obj : T
        The object to check.
        
    types : Types
        A tuple of types to compare against the object.
        
    guards : IterFunc, optional
        Iterable of guard functions.
        
    attrs : dict, optional
        Dictionary of attributes and their expected values to check in the object.
        
    hasattrs : IterStr, optional
        Iterable of attribute names that the object must have.
        
    allattrs : IterStr, optional
        Iterable of attributes to check if their values are all the same in an iterable object.
        
    mapattrs : AttrGuards, optional
        Mapping of attribute names to guard functions for additional checks.

    Returns
    -------
    GuardIterT
        Returns True if the object matches the specified conditions, False otherwise.
    '''
    # print('_____isalias_____')
    # print(obj, types, guards, attrs, hasattrs, allattrs, mapattrs)
    hasattrs = tuple(hasattrs) + tuple(k for k, v in attrs.items() if k not in allattrs)
    if not all(hasattr(obj, attr) for attr in hasattrs): return False
    
    
    found = any(istype(obj, t, guards) for t in types)
    if not found: return False
    
    for attr, val in attrs.items():
        if not (getattr(obj, attr, None) == val):
            if attr in hasattrs and attr not in (*allattrs, *mapattrs): return False
            # ...
        
        if attr in allattrs and not (isnotiterstr(obj) and isallsame(obj, val)): 
            return False
            # continue
        
        if attr in mapattrs and not (mapattrs[attr](obj, val)):            
            return False
            # continue
        
    return True

# %% ../nbs/03_grds.ipynb 32
def isduck(item) -> TypeGuard[Union['ABCDuck', DuckTypeProtocol]]:
    try: from quac.meta import ABCDuck as Duckie
    except: Duckie = DuckTypeProtocol
    return isinstance(item, (DuckTypeProtocol, Duckie, ))

def isduckspec(item) -> TypeGuard[Union['DuckSpec', DuckSpecProtocol]]:
    try: from quac.spec import DuckSpec as Spec
    except: Spec = DuckSpecProtocol
    return isinstance(item, (DuckSpecProtocol, Spec, ))

def isduckmodule(item) -> TypeGuard[DuckModuleProtocol]:
    try: return issubclass(item, (DuckModuleProtocol, ))
    except: return False

# %% ../nbs/03_grds.ipynb 35
def _islen(cls):
    n = getattr(cls, __N__, 0)
    def islen(obj: O, val: int = n) -> TypeGuard[cls]: 
        return len(obj) == val
    islen.__name__ = islen.__qualname__ = f'islen{n}'
    return islen

# %% ../nbs/03_grds.ipynb 37
def _istkey(cls, typek: O = None):
    '''TypeGuard to check if an object is a dict with keys of a specific type'''
    k = getattr(cls, __K__, typek)
    def istkey(obj: T, key: O = k) -> TypeGuard[cls]: 
        if k is None: return isdict(obj)
        return isdict(obj) and all(isinstance(k, (key, )) for k in obj.keys())
    istkey.__name__ = istkey.__qualname__ = f'istkey{k}'
    return istkey

def _istval(cls, typev: O = None):
    '''TypeGuard to check if an object is a dict with values of a specific type'''
    v = getattr(cls, __V__, typev)
    def istkey(obj: T, val: O = v) -> TypeGuard[cls]: 
        if v is None: return isdict(obj) 
        return isdict(obj) and all(isinstance(v, (val, )) for v in obj.values())
    
    istkey.__name__ = istkey.__qualname__ = f'istval{v}'
    return istkey

def _iskvdict(cls):
    (tk, tv) = getattr(cls, __KVTYPES__, (None, None))
    k = getattr(cls, __K__, tk)
    v = getattr(cls, __V__, tv)
    istkeys = _istkey(cls, k)
    istvals = _istval(cls, v)
    def iskvdict(obj: O, val: tuple = (k, v)) -> TypeGuard[cls]: 
        return isdict(obj) and istkeys(obj) and istvals(obj)
    iskvdict.__name__ = iskvdict.__qualname__ = f'is_{k}_{v}_dict'
    return iskvdict
