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

# %% auto 0
__all__ = ['DOT', 'NIL', 'GET', 'HAS', 'RET', 'ATTR', 'SELF', 'ITEM', 'BASES', 'CHECK', 'GUARD', 'RETHAS', 'RETSELF', 'OPTION',
           'DEFAULT', 'CHECKOBJ', 'GUARDOBJ', 'CHECK_ATTR', 'ATTR_GUARD', 'CHECK_ITEM', 'ITEM_GUARD', 'RET_OPTION',
           'RETURN_FNC', 'ATTR_MODE', 'NAMESPACE', 'CLASSVARS', 'ASSIGNED', 'T', 'O', 'P', 'GuardLike',
           'ReturnFunction', 'AttributeMode', 'ReturnOption', 'iscall', 'isnone', 'notnone', 'isiter', 'isstr',
           'allstrs', 'istuple', 'make_return_fnc', 'insattr', 'inspect_attribute', 'inspect_attr', 'ins_attr',
           'get_or_has', 'astrmeta', 'astr', 'attr', 'attrstr', 'attribute_meta', 'attribute']

# %% ../nbs/00_core.ipynb 6
from abc import abstractmethod
from functools import wraps
from enum import StrEnum, auto

# %% ../nbs/00_core.ipynb 8
from typing import (
    Any, Self, Type, Union, TypeVar, ParamSpec, 
    TypeAlias, TypeGuard, ClassVar, Callable, Optional, Iterable,
)

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


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


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


# %% ../nbs/00_core.ipynb 16
DOT = '.'
NIL = ''

GET = 'get'
HAS = 'has'
RET = 'ret'

ATTR = 'attr' 
SELF = 'self'
ITEM = 'item'

BASES = 'bases' 

CHECK = 'check'
GUARD = 'guard'

RETHAS = f'{RET}{HAS}' 
RETSELF = f'{RET}{SELF}'

OPTION = 'option'
DEFAULT = 'default'

CHECKOBJ = 'checkobj'
GUARDOBJ = 'guardobj'

CHECK_ATTR = f'{CHECK}_{ATTR}'
ATTR_GUARD = f'{ATTR}_{GUARD}'
CHECK_ITEM = f'{CHECK}_{ITEM}'
ITEM_GUARD = f'{ITEM}_{GUARD}'
RET_OPTION = f'{RET}_{OPTION}'
RETURN_FNC = 'return_fnc'


ATTR_MODE = f'{ATTR}_mode'

NAMESPACE = 'namespace'

__DOC__ = '__doc__' 
__NAME__ = '__name__'
__MODULE__ = '__module__'
__QUALNAME__ = '__qualname__'
__ANNOTATIONS__ = '__annotations__'

# CLASSVARS = (ATTR, CHECK, GUARD, CHECKOBJ, GUARDOBJ, RETSELF, DEFAULT)
CLASSVARS = (ATTR, CHECK_ATTR, ATTR_GUARD, CHECK_ITEM, ITEM_GUARD, RET_OPTION, RETURN_FNC, ATTR_MODE, DEFAULT)
ASSIGNED = (__MODULE__, __DOC__, __ANNOTATIONS__)

# %% ../nbs/00_core.ipynb 18
class AttributeMode(StrEnum):
    GET = auto()
    HAS = auto()
    
class ReturnOption(StrEnum):
    '''What to return from the item when inspecting its attribute.
    
    Attributes
    ----------
    val : 'val'
        inspect attribute will return the default value.
    
    bool : 'bool'
        inspect attribute will return the boolean of `hasattr`.
        
    call : 'call'
        inspect attribute will return the identity function `(*args, **kwds) --> item`
        which returns the item it was called on.
    
    self : 'self'
        inspect attribute will return the item it was called with.
        
    item : 'self'
        Alias for self.
        
    has : 'bool'
        Alias for bool.
    
    default : 'val'
        Alias for val.
    '''
    val = auto()
    bool = auto()
    call = auto()
    self = auto()
    has = bool
    item = self
    default = val

# %% ../nbs/00_core.ipynb 20
T = TypeVar('T')

O = TypeVar('O')

P = ParamSpec('P')

GuardLike: TypeAlias = Callable[[O], TypeGuard[T]]
'''TypeAlias for a callable that takes an object of type O and returns a TypeGuard for type T.''';

ReturnFunction: TypeAlias = Callable[[tuple[object, str, Any]], Any]
'''The constructor function `(item, attr, default) -> Any` for making the function returned from inspect attribute.''';

# %% ../nbs/00_core.ipynb 22
def iscall(x) -> TypeGuard[Callable]:
    '''Check if `x` is `Callable`.'''
    return isinstance(x, Callable)

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

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

def isiter(x) -> TypeGuard[Iterable]:
    '''Check if `x` is `Iterable`.'''
    return isinstance(x, Iterable)

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

def allstrs(x) -> TypeGuard[tuple[str, ...]]:
    '''Check if `x` is an iterable of `str`s.'''
    return isinstance(x, Iterable) and all(isstr(a) for a in x)

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

# %% ../nbs/00_core.ipynb 24
def make_return_fnc(item: object, attr: str, default: Any = None) -> Callable[P, object]:
    def identity(*args: P.args, **kwargs: P.kwargs):
        return item
    return identity

# %% ../nbs/00_core.ipynb 25
def insattr(
    item: object, 
    attr: str, 
    default: Any = None,
    
    check_attr: bool = True,
    attr_guard: Optional[GuardLike] = notnone,
    check_item: Optional[bool] = None, 
    item_guard: Optional[GuardLike] = notnone, 
    
    ret_option: ReturnOption = ReturnOption.default,
    return_fnc: Optional[ReturnFunction] = make_return_fnc,
    **kwargs: P.kwargs
) -> Union[Any, bool, object]:
    '''Get an attribute from an object and check_attr its type.
    
    Parameters
    ----------
    item : object
        The object to get the attribute from.
        
    attr : str
        The name of the attribute to get.
        
    default : Any, optional
        The default value to return if the attribute is not found, by default None.
        
    check_attr : bool, default: True
        Whether to check the type of the attribute, by default True.
        
    attr_guard : Optional[GuardLike], default: `notnone`
        The type guard to check the type of the retrieved attribute, by default `notnone`.
        
    check_item : Optional[bool], default: False
        Whether to check the object, by default None.
        
    item_guard : Optional[GuardLike], default: `notnone`
        The type guard to check the type of the object, by default `notnone`.
        
    ret_option : bool, default: ReturnOption.default
        Specifies what to return: the value of the attribute from `item`, a boolean indicating 
        the attribute's existence, or the input `item` itself.
        
    return_fnc : Optional[ReturnFunction], optional
        A function that defines the return behavior when ret_option is set to ReturnOption.call, by default make_return_fnc.
        
    '''
    check_attr = kwargs.get('check_attr', kwargs.get('checkret', check_attr))
    attr_guard = kwargs.get('attr_guard', kwargs.get('guardret', attr_guard))
    
    check_item = kwargs.get('checkobj', check_item)
    item_guard = kwargs.get('guardobj', item_guard)
    
    # Step 1: Check if the item is of the correct type
    if (check_item and iscall(item_guard)) and not item_guard(item): 
        return default
    
    # Step 2: Try and get the attribute
    try: has = hasattr(item, attr)
    except: has = False
    
    if not isstr(attr):
        try: attr = getattr(attr, ATTR, attr)
        except: ...
        
    try: val = getattr(item, attr, default)
    except: val = default
    
    if isnone(val): 
        val = default
    
    return_has = (ret_option == ReturnOption.has)
    # Step 3: Check if the attribute is of the correct type
    passed = (has and check_attr and iscall(attr_guard)) and attr_guard(val)
    if passed: 
        return has if return_has else val
    
    # Step 4: value failed the attr_guard, check_attr if the boolean
    # i.e. typeguard result should be returned
    if return_has:
        return has
    
    # Step 5: value failed the attr_guard, check_attr if the object 
    # should be returned
    return_self = (ret_option == ReturnOption.self)
    passed = (check_item and iscall(item_guard)) and item_guard(item)
    if (return_self and (not check_item or passed)):
        return item
    
    return_call = (ret_option == ReturnOption.call)
    if return_call:
        return return_fnc(item, attr, default)
    
    return default

# %% ../nbs/00_core.ipynb 26
@wraps(insattr, assigned=ASSIGNED)
def inspect_attribute(*args: P.args, **kwargs: P.kwargs):
    '''Alias for `insattr`.'''
    return insattr(*args, **kwargs)

@wraps(insattr, assigned=ASSIGNED)
def inspect_attr(*args: P.args, **kwargs: P.kwargs):
    '''Alias for `insattr`.'''
    return insattr(*args, **kwargs)

@wraps(insattr, assigned=ASSIGNED)
def ins_attr(*args: P.args, **kwargs: P.kwargs):
    '''Alias for `insattr`.'''
    return insattr(*args, **kwargs)

@wraps(insattr, assigned=ASSIGNED)
def get_or_has(*args: P.args, **kwargs: P.kwargs):
    '''Alias for `insattr`.'''
    return insattr(*args, **kwargs)

# %% ../nbs/00_core.ipynb 28
class astrmeta(type):
    '''A metaclass for attributes to extend instance checks and equality.
    
    A metaclass for dynamically generating and handling attributes with enhanced instance checks, equality, and attribute manipulation.
    This metaclass allows for the creation of attribute-centric classes (`astr` instances) that encapsulate logic for accessing, setting, 
    and checking the presence of specified attributes on objects. It supports creating attribute-specific classes directly or 
    through decoration, enabling flexible and powerful attribute manipulation and introspection.
    
    Attributes
    ----------
    attr : str
        The name of the attribute to get, set, or check_attr on an object.
        
    check_attr : bool
        Indicates whether to check_attr the type of the attribute upon retrieval.
        
    attr_guard : GuardLike, optional
        A callable used as a type attr_guard for the retrieved attribute.
        
    check_item : bool, optional
        Indicates whether to check_attr the type of the object before attribute manipulation.
        
    item_guard : GuardLike, optional
        A callable used as a type attr_guard for the object before attribute manipulation.
        
    ret_option : bool
        Determines whether to return the original object if the attribute fails the type attr_guard check_attr.
        
    default : Any
        The default value to return if the attribute is not found or fails the type attr_guard check_attr.
    '''
    
    attr: ClassVar[str]
    '''The name of the attribute to get / set / or check_attr if an object has.'''
    
    check_attr: ClassVar[bool] = True
    '''Whether to check the type of the attribute, by default True.'''
    
    attr_guard: ClassVar[Optional[GuardLike]] = notnone
    '''The type guard to check the type of the retrieved attribute, by default `notnone`.'''
    
    check_item: ClassVar[Optional[bool]] = True
    '''Whether to check the type of the object, by default None.'''
    
    item_guard: ClassVar[Optional[GuardLike]] = notnone
    '''The type guard to check the type of the object, by default `notnone`.'''
    
    ret_option: ClassVar[ReturnOption] = ReturnOption.self
    '''Whether to return the object if the attribute fails the attr_guard, otherwise the default value is returned, by default False.'''
    
    return_fnc: ClassVar[ReturnFunction] = make_return_fnc
    
    attr_mode: ClassVar[AttributeMode] = AttributeMode.GET
    
    default: ClassVar[Any] = None
    '''The default value to return if the attribute is not found, by default None.'''
    
    clsvars: tuple[str, ...] = CLASSVARS
    '''The tuple of class variable names.'''
    
    def __new__(cls, name: str, bases: tuple = tuple(), dct: dict = dict(), *args: P.args, **kwargs: P.kwargs):
        # new = super().__new__(mcls, name, bases, dct, *args, **kwargs)
        kwargs.setdefault('__args', args)
        new = super().__new__(cls, name, bases, dct)
        new.setvars(**kwargs)
        return new
    
    def __init__(cls, name: str, bases: tuple = tuple(), dct: dict = dict(), *args: P.args, **kwargs: P.kwargs):
        super().__init__(name, bases, dct)
        # NOTE: likely unneeded as it is called in __new__
        cls.setvars(**kwargs)
            
    def __init_subclass__(cls, *args: P.args, **kwargs: P.kwargs):
        super().__init_subclass__(*args, **kwargs)
        # NOTE: likely unneeded as it is called in __new__
        cls.setvars(**kwargs)
        
    @classmethod
    @abstractmethod
    def __guard__(cls, ins: object) -> TypeGuard['astrmeta']:
        '''Checks if the instance `ins` has the attribute `cls.attr`.'''
        return cls.has(cls, ins)
    
    def __instancecheck__(cls, __instance: object) -> bool:
        '''Checks if the instance has the attribute `cls.attr`.
        
        Examples
        --------
        >>> # create the foo attribtue via subclassing
        ... class foo(attr):
        ...    attr = 'foo'
        ...
        ... # create two classes that have a foo attribute
        ... class testa: 
        ...    foo = 1; bar = 2; hi = 'no'; qux = dict(a=2);
        ... 
        ... class testb: 
        ...    foo = None; bar = -10; hello = 'yes'; qux = dict(a=5);
        ... 
        ... # test equality and instance check_attr works via astrmeta __eq__ and __instancecheck__ methods
        ... foo == testa, foo == testb, isinstance(testa, foo), isinstance(testb, foo)
        (True, True, True, True)
        '''
        try: sub = issubclass(__instance, cls)
        except: sub = False
        return cls.__guard__(__instance) or sub
    
    def __eq__(cls, ins: object) -> bool:
        '''Checks if the instance has the attribute `cls.attr`.
        
        Examples
        --------
        >>> # create the foo attribtue via subclassing
        ... class foo(attr):
        ...    attr = 'foo'
        ...
        ... # create two classes that have a foo attribute
        ... class testa: 
        ...    foo = 1; bar = 2; hi = 'no'; qux = dict(a=2);
        ... 
        ... class testb: 
        ...    foo = None; bar = -10; hello = 'yes'; qux = dict(a=5);
        ... 
        ... # test equality and instance check_attr works via attrmeta __eq__ and __instancecheck__ methods
        ... foo == testa, foo == testb, isinstance(testa, foo), isinstance(testb, foo)
        (True, True, True, True)
        '''
        if isinstance(ins, cls): 
            return True
        return super().__eq__(ins)
    
    def __hash__(cls) -> int:
        return super().__hash__()
    
    def getvars(cls, **kwargs: P.kwargs) -> dict:
        '''Returns a dictionary of class variables using class defaults if not found in `kwargs`.'''
        return {a: kwargs.get(a, getattr(cls, a, None)) for a in cls.clsvars}
    
    def setvars(cls, **kwargs: P.kwargs):
        '''Set all class variables using their defaults values if not found in `kwargs`.'''
        for k, v in cls.getvars(**kwargs).items():
            setattr(cls, k, v)
        
        clsattr = getattr(cls, ATTR, NIL)
        clsname = getattr(cls, __NAME__, NIL).lower()
        # no attr but class has a name, use that for the attribute
        if not clsattr and clsname: 
            setattr(cls, ATTR, clsname)
        
        # cls.attr is 'attr' but class has a different name (e.g. subclass)
        if clsattr == ATTR and clsname != clsattr:
            setattr(cls, ATTR, clsname)
        return cls
    
    def __make_astr__(cls, name: str, *args: P.args, attr: Optional[str] = None, **kwargs: P.kwargs): 
        '''Make a new attribute class with the given attribute name.'''
        # class name, bases and dct
        kwargs.setdefault(ATTR, attr or name)
        bases = kwargs.pop(BASES, (cls, ))
        ndict = kwargs.pop(NAMESPACE, {})
        ndict.update(__annotations__ = cls.__annotations__, __module__ = cls.__module__, attr = attr)
        return cls.__class__(name, bases, ndict, *args, **kwargs)
    
    def __make_astrs__(cls, attrs: tuple[str, ...] = tuple(), *args: P.args, **kwargs: P.kwargs):
        attrs = attrs or (getattr(cls, __NAME__, None), )
        return tuple(cls.__make_astr__(
            getattr(cls, __NAME__, attr), *args, **kwargs, 
            attr=attr) for attr in attrs
        )
    
    def __make_or_call__(cls, *args: P.args, **kwargs: P.kwargs):
        len0 = len(args) == 0
        arg0 = (args if len0 else args[0])
        # no args, create a single attribute using the class __name__ as the attribute name
        if len0:
            return cls.__make_astrs__(*args, **kwargs)
        
        # if here, then there are arguments
        elif allstrs(args):
            # args are all strings (e.g. names of attributes), make many attributes
            attrs = cls.__make_astrs__(attrs=args, **kwargs)
            return attrs[0] if len(attrs) == 1 else attrs
        
        elif istuple(arg0) and allstrs(arg0) and allstrs(args[1:]):
            # args0 is a tuple of all strings (e.g. names of attributes) so
            # args is something like: (('attr1', ...), 'attr2', 'attr3', ...)
            return cls.__make_or_call__(*arg0, *args[1:], **kwargs)
        
        elif istuple(arg0) and allstrs(arg0) and len(arg0) == 1:
            return cls.__make_or_call__(*arg0, *args[1:], **kwargs)
        
        elif istuple(arg0) and allstrs(arg0):
            # args0 is a tuple of all strings (e.g. names of attributes)
            attrs = cls.__make_astrs__(attrs=arg0, *args[1:], **kwargs)
            return attrs[0] if len(attrs) == 1 else attrs
        
        else:
            # called with arguments, but not all are strings, use the classes __call__ method
            return cls.__call__(cls, *args, **kwargs)
    
    def __deco__(cls, **kwargs: P.kwargs):
        def decorator(sub: type):
            return cls.__make_astr__(sub.__name__, bases=(cls, ), **kwargs)
        return decorator
    
    @wraps(insattr)
    def get(cls, item: object) -> Union[Any, bool, object]:
        '''Get the attribute specified by the this class from the object.'''
        if DOT in cls.attr: 
            return cls.dot(item, ret_option = ReturnOption.default)
        kwds = {**cls.getvars(), **dict(ret_option = ReturnOption.default)}
        return insattr(item, **kwds)
    
    @wraps(insattr)
    def has(cls, item: object) -> TypeGuard[bool]:
        '''Returns whether the object has the attribute specified by the this class.'''
        if DOT in cls.attr: return cls.dot(item, ret_option = ReturnOption.has)
        kwds = {**cls.getvars(), **dict(ret_option = ReturnOption.has)}
        return insattr(item, **kwds)
    
    def set(cls, item: object, val: Any) -> Type[Self]:
        '''Set the attribute specified by the this class on the object to the value `val`.'''
        setattr(item, cls.attr, val)
        return cls
    
    def dot(cls, item: object, **kwargs:P.kwargs):
        '''Returns get(...) --> get(...) --> ... --> get / has for a dotted attribute name e.g. `attr1.attr2.attr3`.'''
        names = getattr(cls, ATTR, NIL).split(DOT)
        return cls.chain(item, *names, **kwargs)
    
    def chain(cls, item: object, *names: P.args, **kwargs: P.kwargs):
        '''Returns chain cls.get until for each attribute in *names until the end, then call get / has 
        for a dotted attribute name e.g. `attr1.attr2.attr3`.'''
        kwds = cls.getvars(**kwargs)
        retopt = kwargs.get('return_opt')
        rethas = kwargs.get(RETHAS, False)
        if rethas: 
            retopt = ReturnOption.has
            
        for i, name in enumerate(names):
            cur = dict(attr=name, rethas=False, ret_option=True)
            for key in ('attr_guard', 'item_guard'):
                val = kwds.get(key, None)
                if not isiter(val):
                    kwds[key] = notnone
                elif isiter(val) and i >= len(val):
                    kwds[key] = notnone
                elif isiter(val) and i < len(val):
                    cur[key] = val[i]
                    
            kws = {**kwds, **cur}
            if i == len(names) - 1: 
                kws.update(return_opt = retopt)
            item = insattr(item, **kws)
        return item
    
    def __repr__(cls) -> str:
        return f'{cls.__name__}({cls.attr})'
    
    def __str__(cls) -> str:
        return f'{cls.__name__}({cls.attr})'

# %% ../nbs/00_core.ipynb 30
class astr(metaclass=astrmeta):
    '''Provides a flexible and powerful interface for getting, setting, and checking if an object has a specific attribute.
    
    Instances of `astr` (or subclasses thereof) represent specific attributes and encapsulate logic for accessing, setting, and 
    verifying the presence of these attributes on objects. This enables expressive and concise attribute manipulation and 
    introspection. `astr` classes support creating attribute-specific behavior directly or through decoration, offering a 
    versatile tool for object attribute management.

    Attributes
    ----------
    attr : str
        The name of the attribute to get, set, or check_attr on an object.
        
    check_attr : bool
        Indicates whether to check_attr the type of the attribute upon retrieval.
        
    attr_guard : GuardLike, optional
        A callable used as a type attr_guard for the retrieved attribute.
        
    check_item : bool, optional
        Indicates whether to check_attr the type of the object before attribute manipulation.
        
    item_guard : GuardLike, optional
        A callable used as a type attr_guard for the object before attribute manipulation.
        
    ret_option : bool
        Determines whether to return the original object if the attribute fails the type attr_guard check_attr.
        
    default : Any
        The default value to return if the attribute is not found or fails the type attr_guard check_attr.
    
    Notes
    -----
    `astr` can be subclassed to create attribute-specific classes that include logic for getting, 
    setting, and checking attributes on objects. It supports dot notation for nested attributes and 
    can be used directly or as a decorator to enhance classes with attribute-specific logic.
    
    Examples
    --------
    >>> class foo(attr):
    ...    attr = 'foo'
    
    >>> class bar(attr):
    ...    attr = 'bar'
    
    >>> # Decorator usage to enhance a class with attribute-specific logic
    ... @attr.deco(check_attr=False)
    ... class quz: ...
    '''
    attr: ClassVar[str]
    '''The name of the attribute to get / set / or check_attr if an object has.'''
    
    check_attr: ClassVar[bool] = True
    '''Whether to check_attr the type of the attribute, by default True.'''
    
    attr_guard: ClassVar[Optional[GuardLike]] = notnone
    '''The attr_guard to check_attr the type of the retrieved attribute, by default `notnone`.'''
    
    check_item: ClassVar[Optional[bool]] = True
    '''The attr_guard to check_attr the type of the object, by default None.'''
    
    item_guard: ClassVar[Optional[GuardLike]] = notnone
    '''The attr_guard to check_attr the type of the object, by default `notnone`.'''
    
    ret_option: ClassVar[ReturnOption] = ReturnOption.self
    '''Whether to return the object if the attribute fails the attr_guard, otherwise the default value is returned, by default False.'''
    
    default: ClassVar[Any] = None
    '''The default value to return if the attribute is not found, by default None.'''
    
    return_fnc: ClassVar[ReturnFunction] = make_return_fnc
    
    attr_mode: ClassVar[AttributeMode] = AttributeMode.GET
    
    clsvars: tuple[str, ...] = CLASSVARS
    '''The tuple of class variable names.'''
    
    def __new__(cls, *args: P.args, **kwargs: P.kwargs):
        if args: return cls.many(*args, **kwargs)
        if kwargs: return cls.deco(**kwargs)
        new = super().__new__(cls)
        return new
    
    
    def __call__(self, item: object, *args: P.args, **kwargs: P.kwargs) -> object:
        rethas = kwargs.get(HAS, self.attr_mode == AttributeMode.HAS)
        if rethas == ReturnOption.has:
            return self.has(item)
        return self.get(item)
    
    def __init_subclass__(cls, *args: P.args, **kwargs: P.kwargs):
        super().__init_subclass__()
        # NOTE: likely unneeded as it is called in __new__
        cls.setvars(**kwargs)
        
    @classmethod
    @abstractmethod
    def __guard__(cls, ins: object) -> TypeGuard[Type[Self]]:
        '''Checks if the instance `ins` has the attribute `cls.attr`.'''
        return cls.has(ins, )
    
    @classmethod
    def many(
        cls, attrs: tuple[str, ...] = tuple(), 
        *args: P.args, **kwargs: P.kwargs
    ) -> (tuple[type[Self], ...] | type[Self]):
        if isinstance(attrs, str): attrs = tuple((attrs, ))
        attrs: tuple['astr', ...] = cls.__make_or_call__(attrs, *args, **kwargs)
        if istuple(attrs) and len(attrs) == 1: return attrs[0]
        return attrs
    
    @classmethod
    def deco(cls: Type[Self], **kwargs: P.kwargs) -> Type[Self]:
        return cls.__deco__(**kwargs)

# %% ../nbs/00_core.ipynb 32
@wraps(astr, assigned=ASSIGNED, updated=()) 
class attr(astr):
    '''An alias for the `astr` class.
    
    See Also
    --------
    astr : A class for getting, setting and checking if an object has an attribute.
    '''
    
@wraps(astr, assigned=ASSIGNED, updated=()) 
class attrstr(astr):
    '''An alias for the `astr` class.
    
    See Also
    --------
    astr : A class for getting, setting and checking if an object has an attribute.
    '''

# %% ../nbs/00_core.ipynb 33
class attribute_meta(type):
    clsvars: tuple[str, ...] = CLASSVARS
    '''The tuple of class variable names.'''
    def __new__(cls, name: str, bases: tuple = tuple(), dct: dict = dict(), *args: P.args, **kwargs: P.kwargs):
        kwargs.setdefault('__args', args)
        new = super().__new__(cls, name, bases, dct)
        new.setvars(**kwargs)
        return new
    
    def getvars(cls, **kwargs: P.kwargs) -> dict:
        '''Returns a dictionary of class variables using class defaults if not found in `kwargs`.'''
        return {a: kwargs.get(a, getattr(cls, a, None)) for a in cls.clsvars}
    
    def setvars(cls, **kwargs: P.kwargs):
        '''Set all class variables using their defaults values if not found in `kwargs`.'''
        for k, v in cls.getvars(**kwargs).items(): setattr(cls, k, v)
        clsattr = getattr(cls, ATTR, NIL)
        clsname = getattr(cls, __NAME__, NIL).lower()
        # no attr but class has a name, use that for the attribute
        if not clsattr and clsname: setattr(cls, ATTR, clsname)
        return cls
    
    def __init__(cls, name: str, bases: tuple = tuple(), dct: dict = dict(), *args: P.args, **kwargs: P.kwargs):
        super().__init__(name, bases, dct)
        # NOTE: likely unneeded as it is called in __new__
        cls.setvars(**kwargs)
            
    def __init_subclass__(cls, *args: P.args, **kwargs: P.kwargs):
        super().__init_subclass__(*args, **kwargs)
        # NOTE: likely unneeded as it is called in __new__
        cls.setvars(**kwargs)
    
    
    
    def inspect(cls, item: object, attr: str, **kwargs):
        kwds = cls.getvars(attr=attr, **kwargs)
        return insattr(item, **kwds)
    
    def __repr__(cls) -> str:
        return f'{cls.__name__}({cls.attr})'
    
    def __str__(cls) -> str:
        return f'{cls.__name__}({cls.attr})'
    
    def __call__(self, 
        item: object,
        default: Any = None,
        check_attr: bool = True,
        attr_guard: Optional[GuardLike] = notnone,
        check_item: Optional[bool] = None, 
        item_guard: Optional[GuardLike] = notnone, 
        ret_option: ReturnOption = ReturnOption.default,
        return_fnc: Optional[ReturnFunction] = make_return_fnc,
        **kwargs: P.kwargs
    ):
        return self.inspect(item, self.attr,
            default=default, check_attr=check_attr, attr_guard=attr_guard, check_item=check_item, 
            item_guard=item_guard, ret_option=ret_option, return_fnc=return_fnc, **kwargs
        )
    
class attribute(metaclass=attribute_meta):
    ...
    # def __new__(cls, *args: P.args, **kwargs: P.kwargs):
    #     new = super().__new__(cls)
    #     return new
