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

# %% auto 0
__all__ = ['guard', 'tset', 'tlist', 'tdict', 'ttype', 'ttuple', 'tunion', 'tliteral', 'tgeneric', 'tclassvar', 'eqtype',
           'eqlist', 'eqtuple', 'equnion', 'eqliteral', 'checknan', 'trychecks', 'tryiter', 'isstrbytes', 'notstrbytes',
           'isstrq', 'notiterstr', 'isallsame', 'aliascheck']

# %% ../nbs/02_grds.ipynb 6
import os, inspect
from functools import wraps

# %% ../nbs/02_grds.ipynb 8
from types import NoneType 
from typing import (
    Any, Set, Dict, List, Tuple, Union, ClassVar, Generic, Literal, 
    Callable, Iterable, get_args, get_origin, Type, TypeGuard,
)

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


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


# %% ../nbs/02_grds.ipynb 14
from .type import (T, GuardFunc, StrFactory,)
from .util import (guardname, guarddocs, guardanns,)

# %% ../nbs/02_grds.ipynb 17
def guard(
    *types: Type[T], 
    docsfn: StrFactory = guarddocs, 
    namefn: StrFactory = guardname, 
    annsfn: StrFactory = guardanns,
    negate: bool = False,
    optional: bool = False,
) -> Callable[[Callable[[Any], bool]], Callable[[Any], TypeGuard[T]]]:
    '''Creates a guard decorator based on the provided types and options.

    Parameters
    ----------
    types : Type[T]
        A variable number of types to be used in the guard.
    docsfn : StrFactory, optional
        Function to generate the docstring, by default guarddocs.
    namefn : StrFactory, optional
        Function to generate the name, by default guardname.
    annsfn : StrFactory, optional
        Function to generate the annotations, by default guardanns.
    negate : bool, optional
        Indicates if the guard is for negation, by default False.
    optional : bool, optional
        Indicates if the type can be optional (i.e., include None), by default False.

    Returns
    -------
    Callable[[Callable[[Any], bool]], Callable[[Any], TypeGuard[T]]]
        A decorator that applies the guard to a function.
    '''
    dtypes = (*types, ) + ((NoneType, ) if optional else ())
    def decorator(func:  Callable[[Any], bool]) -> Callable[[Any], TypeGuard[T]]:
        @wraps(func, updated=())
        def wrapper(x: Any) -> TypeGuard[T]:
            return not isinstance(x, dtypes) if negate == True else isinstance(x, dtypes)
        wrapper.__doc__ = docsfn(*dtypes)
        wrapper.__name__ = namefn(*dtypes)
        wrapper.__annotations__ = annsfn(*dtypes)
        return wrapper
    return decorator

# %% ../nbs/02_grds.ipynb 19
def tset(t: Type) -> bool:
    '''Checks if `t` type is a `Set`.'''
    return get_origin(t) is Set

def tlist(t: Type) -> bool: 
    '''Checks if `t` type is a `List`.'''
    return get_origin(t) is List

def tdict(t: Type) -> bool: 
    '''Checks if `t` type is a `Dict`.'''
    return get_origin(t) is Dict

def ttype(t: Type) -> bool: 
    '''Checks if `t` type is a `Type`.'''
    return get_origin(t) is type

def ttuple(t: Type) -> bool: 
    '''Checks if `t` type is a `Tuple`.'''
    return get_origin(t) is Tuple

def tunion(t: Type) -> bool: 
    '''Checks if `t` type is a `Union`.'''
    return get_origin(t) is Union

def tliteral(t: Type) -> bool: 
    '''Checks if `t` type is a `Literal`.'''
    return get_origin(t) is Literal

def tgeneric(t: Type) -> bool: 
    '''Checks if `t` type is a `Generic`.'''
    return get_origin(t) is Generic

def tclassvar(t: Type) -> bool: 
    '''Checks if `t` type is a `ClassVar`.'''
    return get_origin(t) is ClassVar


# %% ../nbs/02_grds.ipynb 21
def eqtype(val, obj: Type, guard: GuardFunc = tliteral) -> TypeGuard[Type]:
    '''Checks if `val` equals `obj`, `guard`'s typing type, or `obj`'s arguments if it is a typing type.

    Parameters
    ----------
    val : Any
        The value to be checked.

    typ : Type
        The type against which the value is to be checked.

    guard : GuardFunc, optional
        The guard function to apply, by default tliteral.

    Returns
    -------
    TypeGuard[Type]
        True if the value matches the specified type or its variants, False otherwise.
    '''
    if not guard(obj): return False
    try: return any((val == (args := get_args(obj)), val in args, val == obj))
    except: return False
    
def eqlist(val, obj: Type) -> TypeGuard[Type]: 
    '''Checks if `val` equals `obj` or any of obj's `List` arguments.'''
    return eqtype(val, obj, tlist)

def eqtuple(val, obj: Type) -> TypeGuard[Type]: 
    '''Checks if `val` equals `obj` or any of obj's `Tuple` arguments.'''
    return eqtype(val, obj, ttuple)

def equnion(val, obj: Type) -> TypeGuard[Type]: 
    '''Checks if `val` equals `obj` or any of obj's `Union` arguments.'''
    return eqtype(val, obj, tunion)

def eqliteral(val, obj: Type) -> TypeGuard[Type]: 
    '''Checks if `val` equals `obj` or any of obj's `Literal` arguments.'''
    return eqtype(val, obj, tliteral)


# %% ../nbs/02_grds.ipynb 23
from math import isnan, nan
from operator import eq

def checknan(val, obj: Type) -> TypeGuard[nan]:
    if isnan(obj): return isnan(val)
    return False

def trychecks(val, obj: Type) -> TypeGuard[Type]:
    fns = (isinstance, issubclass, eq)
    try: 
        if isnan(obj): fns += (checknan, )
    except: ...
    
    for f in fns:
        try: 
            if f(val, obj): return True            
        except: ...
    return False

def tryiter(val) -> TypeGuard[Iterable]:
    try:
        iter(val)
        return True
    except: return False

# %% ../nbs/02_grds.ipynb 24
@guard(str, bytes)
def isstrbytes(x): ...
@guard(str, bytes, negate=True)
def notstrbytes(x): ...
@guard(str, optional=True)
def isstrq(x): ...

def notiterstr(x) -> TypeGuard[Iterable['NotStrBytes']]:
    '''Checks if `x` is an iterable containing elements other than strings or bytes.'''
    return isinstance(x, Iterable) and notstrbytes(x)    

def isallsame(it: Iterable, dtype: T) -> TypeGuard[Iterable[T]]:
    '''Checks if all elements in an iterable are of the same specified type.'''
    itersafe = tryiter(it)
    if not itersafe: return trychecks(it, dtype)
    for el in it:
        if notiterstr(el):
            try: 
                if not isallsame(el, dtype): return False
            except TypeError: 
                return False            
        elif not trychecks(el, dtype):
            return False
    return True

# %% ../nbs/02_grds.ipynb 26
def aliascheck(obj, types, attrs: dict = dict()):
    '''Checks if an object matches a set of types and specific attributes.

    Parameters
    ----------
    obj : Any
        The object to be checked.
    types : iterable
        An iterable of types to check against the object.
    attrs : dict, optional
        A dictionary of attributes to check on the object, by default an empty dict.

    Returns
    -------
    bool
        True if the object matches the types and attributes, False otherwise.
    '''
    found = False
    for t in types:
        try:
            if (found := isinstance(obj, t)): break
        except TypeError:
            try:
                if (found := eqlist(obj, t)): break
                if (found := eqtuple(obj, t)): break
                if (found := equnion(obj, t)): break
                if (found := eqliteral(obj, t)): break
                if (found := obj == t): break
            except: ...

    if not found: return False
    
    for attr, val in attrs.items():        
        if getattr(obj, attr, None) == val: continue
        if attr in {'types', 'dtype', 'type'} and notiterstr(obj) and isallsame(obj, val): continue                     
        return False        
    return True
