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

# %% auto 0
__all__ = ['iscls', 'notcls', 'istuple', 'isstr', 'isdict', 'isiter', 'isnan', 'ishashable', 'isnotspecial', 'isorigin', 'isargs',
           'istyping', 'isprivate', 'isdunder', 'isnotiterstr', 'istype', 'isallsame', 'isalias']

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

# %% ../nbs/04_grds.ipynb 8
from typing import (
    TypeGuard, Iterable, get_args, get_origin,
    _SpecialForm, Generic, Protocol
)

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


# %% ../nbs/04_grds.ipynb 12
from nlit import __HASH__ 
from uscr import private, dunder

# %% ../nbs/04_grds.ipynb 14
from .cons import ALLATTRS
from ptyp.atyp import (
    T, Types, IterStr, GuardIterT, AttrGuards, GuardIter, IterFunc, IterType, 
    GuardIterType, NanGuard, HasHash, IsNotSpecial, OriginGuard, Guard, OriginTypes, HasIterElements
)

# %% ../nbs/04_grds.ipynb 17
def iscls(x) -> TypeGuard[type]:
    '''Check if `x` is a class.
    
    Notes
    ----- 
    `iscls` is an alias for `inspect.isclass`.
    Class objects provide these attributes:
    
    `__doc__`: 
        documentation string 
        
    `__module__`: 
        name of module in which this class was defined
    
    See Also
    --------
    inspect.isclass : https://docs.python.org/3/library/inspect.html#inspect.isclass
    '''
    return inspect.isclass(x)

def notcls(x) -> TypeGuard[T]:
    '''Check if `x` is not a class.
    
    See Also
    --------
    iscls : Alias for `inspect.isclass`
    inspect.isclass : https://docs.python.org/3/library/inspect.html#inspect.isclass
    '''
    return not iscls(x)

def istuple(o: object) -> TypeGuard[tuple]:
    '''Check if the given object is `tuple`.'''
    return isinstance(o, tuple)

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

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

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

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

# %% ../nbs/04_grds.ipynb 19
def ishashable(o: object) -> HasHash:
    '''Check if the given object is hashable.'''
    return hasattr(o, __HASH__)

def isnotspecial(t: type) -> IsNotSpecial:
    '''Check if the given type is not a special form in the `typing` module.'''
    withorg = get_origin(t)
    hashash = ishashable(t)
    special = isinstance(t, _SpecialForm) or t in (Generic, Protocol)
    return withorg and hashash and not special

def isorigin(t: type) -> OriginGuard:
    '''Check if the given type is one of the origin types defined in `OriginTypes`.'''
    otypes = get_args(OriginTypes)
    return any((get_origin(t) is _ for _ in otypes))

def isargs(o: object, t: T) -> Guard:
    '''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) -> Guard:
    '''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

# %% ../nbs/04_grds.ipynb 21
def isprivate(s: str) -> TypeGuard[private]:
    '''Check if `s` is a private string e.g. `'_{anything}'`.'''
    return isinstance(s, private)

def isdunder(s: str) -> TypeGuard[dunder]:
    '''Check if `s` is an dunder string e.g. `'__{anything}__'`.'''
    return isinstance(s, dunder)

def isnotiterstr(o: object) -> HasIterElements:
    '''Check if the given object is an iterable but not a string or bytes.'''
    return isiter(o) and not isinstance(o, (str, bytes))

# %% ../nbs/04_grds.ipynb 23
def istype(o: object, t: T, guards: IterFunc = tuple(), dropfn: IterFunc = tuple()) -> Guard:
    '''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):
                    # print(f'{comp.__name__}({o}, {t}), True')
                    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/04_grds.ipynb 25
def isallsame(it: IterType, dtype: T) -> GuardIterType:
    '''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/04_grds.ipynb 27
def isalias(
    obj: T,       # object to check
    types: Types, # types to check
    guards: IterFunc = tuple(),
    attrs: dict = dict(),
    hasattrs: IterStr = tuple(),  # attributes to check
    allattrs: IterStr = ALLATTRS, # attributes to check that values are all the same if object is iterable
    mapattrs: AttrGuards = dict(),
) -> GuardIterT:
    '''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.
    '''
    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):
            # return False
            continue
        
        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
