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

# %% auto 0
__all__ = ['T', 'P', 'Types', 'TypeLike', 'Guard', 'GuardFunc', 'guardtype', 'NotMeta', 'Not', 'NTyp', 'negate', 'nottypes',
           'NotStrBytes', 'notiterstr', 'alldtype']

# %% ../nbs/00_core.ipynb 6
import os, inspect
from functools import wraps

# %% ../nbs/00_core.ipynb 8
from typing import (
    Any, Type, Tuple, Union, Callable, Iterable, TypeGuard, Protocol, _ProtocolMeta,
    runtime_checkable, TypeVar, ParamSpec, TypeAlias, Self, Optional,
    Generic, GenericAlias
)

# %% ../nbs/00_core.ipynb 10
#| export


# %% ../nbs/00_core.ipynb 12
#| export


# %% ../nbs/00_core.ipynb 14
#| export


# %% ../nbs/00_core.ipynb 16
T = TypeVar('T')
P = ParamSpec('P')

Types: TypeAlias = Tuple[Type, ...]
'''`*args` type''';

TypeLike: TypeAlias = Union[type, Type, Type[T], TypeAlias]
'''type, Type, type of type, TypeAlias, or `*args` type''';

Guard: TypeAlias = TypeGuard[TypeLike]
'''Generic TypeGuard''';

GuardFunc: TypeAlias = Callable[P, Guard]
'''Generic TypeGuard function''';

# %% ../nbs/00_core.ipynb 18
def guardtype(*types: Types) -> Guard:
    '''Creates a TypeGuard based on the provided types.
    
    Parameters
    ----------
    types : Types
        A tuple of types to be included in the TypeGuard.
    
    Returns
    -------
    Guard
        A TypeGuard that represents either None (if no types are provided),
        a single type (if one type is provided), or a Union of the provided types.
    '''
    if len(types) == 0:  return TypeGuard[None]
    elif len(types) == 1: return TypeGuard[types[0]]
    else: return TypeGuard[Union[types[0], *types[1:]]]

# %% ../nbs/00_core.ipynb 20
class NotMeta(_ProtocolMeta):
    '''A metaclass for creating negated type classes.

    This metaclass overrides the instance check to use a custom 'check' method.
    It also provides utility methods for getting class names and setting types.

    Attributes
    ----------
    types : Types
        Tuple of types that are negated in the check.

    Methods
    -------
    check(self, obj) -> Guard
        Custom instance check method for negated types.
        
    getclsname(cls, clsname) -> Union[str, bool]
        Retrieves the class name for the negated type.

    getqualname(cls, *types, clsname, shownot, notstr) -> str
        Generates the qualified name for the negated type.

    settypes(cls, *types, clsname, shownot, notstr)
        Sets the types for the negated type class.
    '''
    types: Types = tuple()       

    @classmethod
    def default_kwds(cls, **kwargs) -> dict:
        kwds = dict(
            notstr  = kwargs.get('notstr', '~'),
            clsname = kwargs.get('clsname', True),
            shownot = kwargs.get('shownot', True),
        )
        return kwds

    def __new__(mcls, name, bases, dct, **kwargs):
        '''Create a new class instance.'''        
        kwds = mcls.default_kwds(**kwargs)
        dct['types'] = (types := kwargs.pop('types', tuple()))
        dct['check'] = classmethod(lambda cls, obj: not isinstance(obj, types))
        new = super().__new__(mcls, name, bases, dct)
        new.__qualname__ = new.getqualname(*types, **kwds)
        new.settypes(*types, **kwds)
        return new

    def __call__(cls, *dtypes: Types, **kwargs):
        '''Allows using Not as a decorator with types to negate.'''        
        def decorator(kls):
            '''Decorator to apply negation to a class.'''
            clsname = kwargs.get('clsname', kls.__name__)
            bases = (kls, Not) if Not not in kls.__bases__ else (kls,)
            
            dct = dict(check = classmethod(lambda cls, obj: not isinstance(obj, dtypes)))
            new = cls.__class__(clsname, bases, dct, types=dtypes, **kwargs)
            return new
        return decorator

    def __instancecheck__(self, ins) -> bool: return self.check(ins)
    
    # @classmethod
    def check(self, obj) -> Guard: return not isinstance(obj, self.types)

    # @classmethod
    def getclsname(cls, clsname: Union[str, bool] = True) -> Union[str, bool]:
        if clsname == False: return clsname
        if inspect.isclass(clsname): return clsname.__name__
        elif isinstance(clsname, str): return clsname
        else: return cls.__name__
        
    # @classmethod
    def getqualname(
        cls, *types: Types, clsname: Union[str, bool] = True,
        shownot: bool = True, notstr: str = '~'
    ) -> str:
        tname = ', '.join([str(arg.__name__) for arg in types or tuple()])
        qname = f'{{{tname}}}'
        if shownot: qname = f'{notstr}{qname}'
        if (cname := cls.getclsname(clsname)): qname = f'{cname}({qname})'
        return qname
    
    # @classmethod
    def settypes(
        cls, *types: Types, clsname: str | bool = True,
        shownot: bool = True, notstr: str = '~'
    ):
        qname = cls.getqualname(*types, clsname=clsname, shownot=shownot, notstr=notstr)
        setattr(cls, '__qualname__', qname)
        annots = getattr(getattr(cls, 'check', None), '__annotations__', {})
        annots['return'] = guardtype(*types)
        setattr(cls, 'types', types)

# %% ../nbs/00_core.ipynb 22
@runtime_checkable
class Not(Protocol, metaclass=NotMeta):
    '''A Protocol class for creating negated type checks.

    This class uses the NotMeta metaclass and can be subclassed to create custom
    negated type checks.

    

    Class Methods
    -------------
    __init_subclass__(cls, *args, types, **kwargs) -> None
        Automatically called when subclassing. Sets the types for negation.
        
    negate(cls, *dtypes, **kwargs)
        A decorator factory for creating subclasses with negated type checks.
    '''
    @classmethod
    def __init_subclass__(cls, *args, types: Types = tuple(), **kwargs) -> None:
        super().__init_subclass__()
        kwds = cls.default_kwds(**kwargs)
        cls.settypes(*types, **kwds)

    @classmethod
    def negate(cls, *dtypes: Types, **kwargs):
        def decorator(kls):
            kwargs.setdefault('clsname', cls.__name__)
            return cls(*dtypes, **kwargs)(kls)
        return decorator

# %% ../nbs/00_core.ipynb 23
@wraps(Not, updated=())
class NTyp: ...

# %% ../nbs/00_core.ipynb 25
def negate(*dtypes: Types, **kwargs) -> Callable:
    '''A decorator factory for applying negated type checks to a class.
    
    Parameters
    ----------
    dtypes : Types
        Types to be negated.

    kwargs : dict
        Additional keyword arguments passed to the Not.negate method.
    
    Returns
    -------
    Callable
        A decorator that when applied to a class, enhances it with negated type checking.
    '''
    def decorator(cls):
       kwargs.setdefault('clsname', cls.__name__)
       return Not(*dtypes, **kwargs)(cls)       
    return decorator

def nottypes(*dtypes: type, **kwargs) -> Callable:
    '''A decorator for setting negated types on a class.
    
    Parameters
    ----------
    dtypes : tuple of type
        Types to be negated.
    kwargs : dict
        Additional keyword arguments for customization.
    
    Returns
    -------
    Callable
        A decorator that sets the specified negated types on a class.
    '''
    def decorator(cls):
        kwargs.setdefault('clsname', cls.__name__)
        return Not(*dtypes, **kwargs)(cls)
    return decorator

# %% ../nbs/00_core.ipynb 30
@Not(str, bytes)
class NotStrBytes(Not): ...
NotStrBytes

# %% ../nbs/00_core.ipynb 32
def notiterstr(x) -> TypeGuard[Iterable[NotStrBytes]]:
    return isinstance(x, Iterable) and isinstance(x, NotStrBytes)
    return isinstance(x, Iterable) and NotStrBytes.check(x)

def alldtype(it: Iterable, dtype: type):
    for el in it:
        if notiterstr(el):
            if not alldtype(el, dtype): return False
        elif not isinstance(el, dtype): return False
    return True
