Source code for kwhelp.decorator

import functools
from typing import Any, Dict, Iterable, Iterator, List, Optional, Set, Tuple, Union
from enum import Enum, IntEnum, IntFlag, auto
from collections import OrderedDict
from inspect import signature, isclass, Parameter, Signature
from logging import Logger, LoggerAdapter
from ..checks import TypeChecker, RuleChecker, SubClassChecker
from ..rules import IRule
from ..helper import is_iterable, Formatter
from ..exceptions import RuleError
from ..helper import NO_THING
# import wrapt


[docs]class DecFuncEnum(IntEnum): """Represents options for type of Function or Method""" FUNCTION = 1 """Normal Unbound function""" METHOD_STATIC = 2 """Class Static Method (@staticmethod)""" METHOD = 3 """Class Method""" METHOD_CLASS = 4 """Class Method (@classmethod)""" PROPERTY_CLASS = 5 """Class Property (@property)""" def __str__(self): return self._name_
[docs]class DecArgEnum(IntFlag): """Represents options for the type of function arguments to process""" ARGS = auto() """Process ``*args``""" KWARGS = auto() """Process ``**kwargs``""" NAMED_ARGS = auto() """Process named keyword args""" NO_ARGS = NAMED_ARGS | KWARGS """Process Named Keyword args and ``**kwargs`` only""" All_ARGS = ARGS | KWARGS | NAMED_ARGS """Process All Args"""
class _FuncInfo(object): # foo(*args) # assert info.index_args == 0 # assert info.index_kwargs == -1 # assert len(info.lst_kw_only) == 0 # assert len(info.lst_pos_only) == 0 # assert len(info.lst_pos_or_kw) == 0 # # foo(**kwargs) # assert info.index_args == -1 # assert info.index_kwargs == 0 # assert len(info.lst_kw_only) == 0 # assert len(info.lst_pos_only) == 0 # assert len(info.lst_pos_or_kw) == 0 # # foo(one, two, three, four) # assert info.index_args == -1 # assert info.index_kwargs == -1 # assert len(info.lst_kw_only) == 0 # assert len(info.lst_pos_only) == 0 # self.assertListEqual(info.lst_pos_or_kw, ['one', 'two', 'three', 'four']) # # foo(one, two, three, four, **kwargs) # assert info.index_args == -1 # assert info.index_kwargs == 4 # assert len(info.lst_kw_only) == 0 # assert len(info.lst_pos_only) == 0 # self.assertListEqual(info.lst_pos_or_kw, ['one', 'two', 'three', 'four']) # # foo(*args, one, two, three, four, **kwargs) # assert info.index_args == 0 # assert info.index_kwargs == 5 # self.assertListEqual(info.lst_kw_only, ['one', 'two', 'three', 'four']) # assert len(info.lst_pos_only) == 0 # assert len(info.lst_pos_or_kw) == 0 # # foo(neg_two, neg_one, *args, one, two, three, four, **kwargs) # assert info.index_args == 2 # assert info.index_kwargs == 7 # self.assertListEqual(info.lst_kw_only, ['one', 'two', 'three', 'four']) # assert len(info.lst_pos_only) == 0 # self.assertListEqual(info.lst_pos_or_kw, ['neg_two', 'neg_one']) # region init def __init__(self, func: callable, ftype: DecFuncEnum): self._len_pos_only = None self._len_kw_only = None self._len_pos_or_kw = None self._all_keys: Union[None, Tuple[str]] = None self.index_args: int = -1 self.index_kwargs = -1 self.lst_pos_only: List[str] = [] self.lst_kw_only: List[str] = [] self.lst_pos_or_kw: List[str] = [] self.signature: Signature = signature(func) self._name = func.__name__ self.ftype = ftype self._defaults = {} self._set_info() def _drop_arg_first(self) -> bool: return self.ftype.value > DecFuncEnum.METHOD_STATIC.value def _set_info(self): drop_first = self._drop_arg_first() i = 0 for k, v in self.signature.parameters.items(): if drop_first and i == 0: i += 1 continue if v.kind == v.VAR_POSITIONAL: # args self.index_args = i if drop_first: self.index_args -= 1 elif v.kind == v.VAR_KEYWORD: # kwargs self.index_kwargs = i if drop_first: self.index_kwargs -= 1 elif v.kind == v.KEYWORD_ONLY: self.lst_kw_only.append(v.name) if v.default != v.empty: self._defaults[k] = v.default elif v.kind == v.POSITIONAL_ONLY: # pragma: no cover self.lst_pos_only.append(v.name) if v.default != v.empty: self._defaults[k] = v.default elif v.kind == v.POSITIONAL_OR_KEYWORD: self.lst_pos_or_kw.append(v.name) if v.default != v.empty: self._defaults[k] = v.default i += 1 # endregion init # region public Methods def is_default(self, key: str) -> bool: """Gets if key exist in defaults""" return key in self._defaults # endregion public Methods # region Property @property def defauts(self) -> Dict[str, Any]: """Defalut value for any args that has defaults""" return self._defaults @property def len_positon_only(self) -> int: """Length of Position only args""" if self._len_pos_only is None: self._len_pos_only = len(self.lst_pos_only) return self._len_pos_only @property def len_kw_only(self) -> int: """Length of Keyword only args""" if self._len_kw_only is None: self._len_kw_only = len(self.lst_kw_only) return self._len_kw_only @property def len_pos_or_kw(self) -> int: """Length of Position or Keyword args""" if self._len_pos_or_kw is None: self._len_pos_or_kw = len(self.lst_pos_or_kw) return self._len_pos_or_kw @property def is_args_only(self) -> bool: """Gets if function is *args only""" if self.index_args != 0: return False if self.index_kwargs >= 0: return False result = True result = result and self.len_positon_only == 0 result = result and self.len_kw_only == 0 result = result and self.len_pos_or_kw == 0 return result @property def is_kwargs_only(self) -> bool: """Gets if function is **kwargs only""" if self.index_args >= 0: return False if self.index_kwargs != 0: return False result = True result = result and self.len_positon_only == 0 result = result and self.len_kw_only == 0 result = result and self.len_pos_or_kw == 0 return result @property def is_named_args_only(self) -> bool: """Gets if function is named only. No *args or **kwargs""" if self.index_args >= 0: return False if self.index_kwargs >= 0: return False result = True result = result and self.len_pos_or_kw > 0 result = result and self.len_positon_only == 0 result = result and self.len_kw_only == 0 return result @property def is_args(self) -> bool: """Gets if function has any *args""" return self.index_args >= 0 @property def is_noargs(self) -> bool: """Gets if function has any arguments after excluding any *args""" if self.is_kwargs: return True count = self.len_kw_only count += self.len_pos_or_kw count += self.len_positon_only return count > 0 @property def is_kwargs(self) -> bool: """Gets if function has any **kwargs""" return self.index_kwargs >= 0 @property def all_keys(self) -> Tuple[str]: """Gets combined keys for ``pos_or_kw``, ``kw_only``, ``pos_only``""" if self._all_keys is None: keys = [] for key in self.lst_pos_or_kw: keys.append(key) for key in self.lst_kw_only: keys.append(key) for key in self.lst_pos_only: # pragma: no cover keys.append(key) self._all_keys = tuple(keys) return self._all_keys @property def is_drop_first(self) -> bool: """Gets of the first arg is to be dropped. Thsi is determined by ftype (DecFuncEnum)""" return self._drop_arg_first() @property def name(self) -> str: """Gets the name of the function""" return self._name # endregion Property class _FnInstInfo(object): # region init def __init__(self, fninfo: _FuncInfo, fn_args: tuple, fn_kwargs: "OrderedDict[str, Any]"): """ [summary] Args: fninfo (_FuncInfo): [description] fn_args (tuple): [description] fn_kwargs (Dict[str, Any]): [description] """ self._fn_info = fninfo self._fn_name = self._fn_info.name self._kw = OrderedDict() self._real_kw = OrderedDict() self._real_args = [] self._cache = {} self._process_kwargs(args=fn_args, kwargs=fn_kwargs) self._process_args(args=fn_args) def _process_kwargs(self, args: tuple, kwargs: Dict[str, Any]): if self._fn_info.is_args_only: return if self._fn_info.is_drop_first: tmp_args = [*args[1:]] else: tmp_args = [*args] def process_kw(keys: Iterable[str]): missing_args = [] ignore_keys = set() for key in keys: if key in self._kw and not key in kwargs: continue try: self._kw[key] = kwargs[key] except KeyError: pass # will be a default if key in self._fn_info.defauts: self._kw[key] = self._fn_info.defauts[key] else: missing_args.append(key) ignore_keys.add(key) if len(missing_args) > 0: self._missing_args_error(missing_names=missing_args) for k, v in kwargs.items(): if not k in ignore_keys: self._real_kw[k] = v return if self._fn_info.is_kwargs_only is True: self._real_kw.update(**kwargs) return if self._fn_info.is_named_args_only is True: if len(tmp_args) > 0: self._kw.update(zip(self._fn_info.lst_pos_or_kw, tmp_args)) process_kw(keys=self._fn_info.lst_pos_or_kw) return # at this point the are kwargs but not all are assigned to real key names. # is it posible at this point that some of the values are contained within args. if self._fn_info.is_args is False: if len(tmp_args) > 0: self._kw.update(zip(self._fn_info.lst_pos_or_kw, tmp_args)) keys = self._fn_info.lst_pos_or_kw process_kw(keys=keys) return # at this point there are definatly args # check to see if there are any pre *args names. if self._fn_info.index_args > 0: if len(tmp_args) > 0 and self._fn_info.len_pos_or_kw > 0: self._kw.update(zip(self._fn_info.lst_pos_or_kw, tmp_args)) process_kw(keys=self._fn_info.lst_kw_only) return # at this point there are *args but they do not contain any keyword arg values. process_kw(keys=self._fn_info.all_keys) def _process_args(self, args: tuple): if self._fn_info.is_args is False: return if self._fn_info.is_drop_first: tmp_args = [*args[1:]] else: tmp_args = [*args] if self._fn_info.is_args_only: self._real_args = tmp_args return if self._fn_info.index_args > 0: # lst_pos_or_kw contains pre *arg keys # if there are key, values after *args then they will be in lst_kw_only tmp_args = tmp_args[self._fn_info.index_args:] self._real_args = tmp_args return self._real_args = tmp_args return # endregion init # region Private Methods def _get_all_kw(self) -> "OrderedDict[str, Any]": """Get all keword args combined into one dictionary""" key = 'all_kw' if key in self._cache: return self._cache[key] kw = OrderedDict(**self.key_word_args) kw.update(self.kwargs) self._cache[key] = kw return self._cache[key] def _missing_args_error(self, missing_names: List[str]): fn_name = self.name + "()" msg = Formatter.get_missing_args_error_msg( missing_names=missing_names, name=fn_name) raise TypeError(msg) # endregion Private Methods # region Public Methods def get_filter_arg(self) -> "OrderedDict[str, Any]": """ Get a dictionary of args only. All arg keys will be in the format of *#. Eg {'*0': 22, '*1': -5.67} Returns: Dict[str, Any]: Args as dictionary """ cache_key = 'filter_arg' if cache_key in self._cache: return self._cache[cache_key] result = OrderedDict() if self.info.is_args is False: return result offset = self.info.index_args for i, arg in enumerate(self.args): key = '*' + str(i + offset) result[key] = arg self._cache[cache_key] = result return self._cache[cache_key] def get_filter_noargs(self) -> "OrderedDict[str, Any]": """ Gets a dictionary of all keyword args that has all plain args omitted. Returns: Dict[str, Any]: dictionary with args ommited """ return self._get_all_kw() def get_filtered_kwargs(self) -> "OrderedDict[str, Any]": """ Gets a dictionary of only kwargs Returns: Dict[str, Any]: dictionary of kwargs only """ key = 'filtered_kwargs' if key in self._cache: return self._cache[key] self._cache[key] = OrderedDict(self.kwargs) return self._cache[key] def get_filtered_key_word_args(self) -> "OrderedDict[str, Any]": """ Gets a dictionary of only keyword args Returns: Dict[str, Any]: dictionary of keyword args only """ key = 'filtered_key_word_args' if key in self._cache: return self._cache[key] self._cache[key] = OrderedDict(self.key_word_args) return self._cache[key] def get_all_args(self) -> "OrderedDict[str, Any]": """ Gets all keyword, kwarg and args in a single dictionary Returns: Dict[str, Any]: dictionay containing all args """ key = 'filtered_all_args' if key in self._cache: return self._cache[key] def get_pre_star_keys() -> Tuple[str]: pre_keys = tuple() if self.info.index_args > 0: # there are keys before *args # when there are pre keys they are held in lst_pos_or_kw pre_keys = tuple(self.info.lst_pos_or_kw) return pre_keys result = OrderedDict() # pre list is needed to preserve order of keys pre = get_pre_star_keys() i = 0 for key in pre: result[key] = self.key_word_args[key] i += 1 for arg in self.args: key = '*' + str(i) result[key] = arg i += 1 for key, value in self.key_word_args.items(): if not key in pre: result[key] = value result.update(self.kwargs) self._cache[key] = result return self._cache[key] # endregion Public Methods # region Properties @property def name(self) -> str: return self._fn_name @property def info(self) -> _FuncInfo: return self._fn_info @property def key_word_args(self) -> "OrderedDict[str, Any]": return self._kw @property def kwargs(self) -> "OrderedDict[str, Any]": return self._real_kw @property def args(self) -> list: return self._real_args # endregion Properties class _CommonBase(object): def __init__(self, **kwargs): """ Constructor Keyword Arguments: opt_return (object, optional): Return value when decorator is invalid. By default an error is rasied when validation fails. If ``opt_return`` is supplied then it will be return when validation fails and no error will be raised. opt_logger (Union[Logger, LoggerAdapter], optional): Logger that logs exceptions when validation fails. """ self._opt_return = kwargs.get("opt_return", NO_THING) self._logger: Optional[Union[Logger, LoggerAdapter]] = kwargs.get("opt_logger", None) # and option check for fn value in kwargs. can be used for testing self._fn: Optional[callable] = kwargs.get('_option_fn', None) def _log_err(self, err: Exception): """ [summary] Args: err (Exception): [description] """ if isinstance(self._logger, Logger): self._logger.exception(err) return sbc = SubClassChecker(LoggerAdapter, raise_error=False) is_valid = sbc.validate(self._logger) if is_valid: self._logger.exception(err) def _is_opt_return(self) -> bool: """ Gets if opt_return value has been set in constructor Returns: bool: True if opt_return value is set; Otherwise, False """ return not self._opt_return is NO_THING def _call_init(self, **kwargs): """ Call Init. Provides args for base methhods. Keyword Arguments: func (callable): Function that is being wrapped """ fn = kwargs.get('func', None) if not fn is None: self._fn = fn # region Properties @property def fn(self) -> callable: if self._fn is None: raise ValueError( "fn has not been set. Check if _call_init is called.") return self._fn # endregion Properties class _DecBase(_CommonBase): # region Init def __init__(self, **kwargs): super().__init__(**kwargs) self._ftype: DecFuncEnum = kwargs.get("ftype", None) if self._ftype is not None: if not isinstance(self._ftype, DecFuncEnum): try: self._ftype = DecFuncEnum(self._ftype) except: raise TypeError( f"{self.__class__.__name__} requires arg 'ftype' to be a 'DecFuncType") else: self._ftype = DecFuncEnum.FUNCTION self._cache = { 'args': [], 'kwargs': OrderedDict() } self._w_cache = None def _call_init(self, **kwargs): """ Call Init. Provides args for base methhods. Keyword Arguments: func (callable): Function that is being wrapped """ super()._call_init(**kwargs) def _wrapper_init(self, **kwargs): """ Wrapper Init. Provides args for base methods. This method usually called right after Wrapper method Keyword Arguments: args (Iterable[object]): Wrapped function ``args`` kwargs (Dict[str, Any]): Wrapped function ``kwargs`` """ clear_cache = kwargs.get("clear_cache", True) if clear_cache: self._w_cache = {} key = 'args' if key in kwargs: self.args = kwargs[key] key = 'kwargs' if key in kwargs: self.kwargs = kwargs[key] # endregion Init # region Property # region Function cache Properties @property def fn_cache(self) -> Dict[str, object]: """Gets function level cache""" if not self._w_cache: self._w_cache = {} return self._w_cache @property def args(self) -> Iterable[object]: """Gets/sets wrapped function args""" return self.fn_cache['args'] @args.setter def args(self, value: Iterable[object]): self.fn_cache['args'] = [*value] @property def kwargs(self) -> Dict[str, Any]: """Gets/sets wrapped function kwargs""" return self.fn_cache['kwargs'] @kwargs.setter def kwargs(self, value: Dict[str, Any]): od = OrderedDict() for k, v in value.items(): od[k] = v self.fn_cache['kwargs'] = od @property def fn_inst_info(self) -> _FnInstInfo: cache = self.fn_cache key = '_fn_instance_info' if key in cache: return cache[key] cache[key] = self._get_inst_info() return cache[key] # endregion Function cache Properties # endregion Property def _drop_arg_first(self) -> bool: return self._ftype.value > DecFuncEnum.METHOD_STATIC.value def _get_args(self): # return self.fn_inst_info.args if self._drop_arg_first(): return self.args[1:] return self.args def _get_args_star(self) -> Iterable[object]: """ Get args accounting for ``*args`` postions in function and if function class method. Args: func (callable): function with args args (Iterable[object]): function current args Returns: Iterable[object]: New args that may be a subset of all of orignial ``args``. """ pos = self._get_star_args_pos() drop_first = self._drop_arg_first() i = 0 if pos > 0: i += pos if drop_first: i += 1 if i > 0: return self.args[i:] return self.args def _get_fn_info(self) -> _FuncInfo: info = self._cache.get("_fn_info", False) if info: return info self._cache['_fn_info'] = _FuncInfo(func=self.fn, ftype=self._ftype) return self._cache['_fn_info'] def _get_inst_info(self, **kwargs) -> _FnInstInfo: """ Gets Function Info Keyword Arguments: error_check (bool, optional): Determinse if errors are raise if there are missing keywords. This is the case when function has keywords without defaults assigned and no value is passed into function. Returns: _FnInstInfo: Function Instance Info """ err_chk = bool(kwargs.get("error_check", True)) try: info = _FnInstInfo(fninfo=self._get_fn_info(), fn_args=self.args, fn_kwargs=self.kwargs) except TypeError as e: if err_chk: msg = str(e) msg += self._get_class_dec_err() raise TypeError(msg) return info def _get_args_dict(self, **kwargs) -> "OrderedDict[str, Any]": """ Gets OrderedDict of all Args, and Keyword args. All ``*arg`` values will have a key of ``*#`` Eg: ``'*0', 12``, ``'*1': 45.77``, ``'*3': 'flat'`` Keyword Arguments: error_check (bool, optional): Determinse if errors are raise if there are missing keywords. This is the case when function has keywords without defaults assigned and no value is passed into function. Returns: OrderedDict[str, Any]: Dictionary of keys and values representing ``func`` keywords and values. """ info = self._get_inst_info(kwargs=kwargs) return info.get_all_args() def _get_filtered_args_dict(self, opt_filter: DecArgEnum = DecArgEnum.All_ARGS) -> "OrderedDict[str, Any]": """ Gets filtered dictionary Args: filter (DecArgEnum): Filter option Returns: OrderedDict[str, Any]: of based on filter """ fn_info = self.fn_inst_info if opt_filter & DecArgEnum.All_ARGS == DecArgEnum.All_ARGS: return fn_info.get_all_args() if opt_filter & DecArgEnum.NO_ARGS == DecArgEnum.NO_ARGS: return fn_info.get_filter_noargs() result = OrderedDict() if DecArgEnum.ARGS in opt_filter: if fn_info.info.is_args: result.update(fn_info.get_filter_arg()) if DecArgEnum.KWARGS in opt_filter: if fn_info.info.is_kwargs: result.update(fn_info.get_filtered_kwargs()) if DecArgEnum.NAMED_ARGS in opt_filter: result.update(fn_info.get_filtered_key_word_args()) return result def _get_formated_types(self, types: Iterator[type], **kwargs) -> str: """ Gets a formated string from a list of types. Args: types (Iterator[type]): Types to create fromated string. Keyword Args: conj (str, optional): Conjunction used to join list. Default ``and``. wrapper (str, optional): String to prepend and append to each value. Default ``'``. Returns: str: Formated String """ t_names = [t.__name__ for t in types] result = Formatter.get_formated_names(names=t_names, **kwargs) return result def _get_star_args_pos(self) -> int: """ Gets the zero base postion of *args in a function. Args: func (callable): function to get *args postion of. Returns: int: -1 if *args not present; Otherwise zero based postion of *args """ info = self._get_fn_info() return info.index_args def _get_class_dec_err(self, **kwargs) -> str: """ Gets a string representing class decorator error. Keyword Args: nl (bool, optional): Determines if new line is prepended to return value. Default ``True`` Returns: str: Formated string similar to ``SubClass decorator error.`` """ nl = kwargs.get('nl', True) result = "" if nl: result = result + '\n' result = result + f"{self.__class__.__name__} decorator error." return result class _RuleBase(_DecBase): def _get_err(self, e: RuleError): err = RuleError.from_rule_error( e, fn_name=self.fn.__name__, msg=self._get_class_dec_err(nl=False)) return err
[docs]class TypeCheck(_DecBase): """ Decorator that decorates methods that requires args to match a type specificed in a list See Also: :doc:`../../usage/Decorator/TypeCheck` """
[docs] def __init__(self, *args: Union[type, Iterable[type]], **kwargs): """ Constructor Other Parameters: args (type): One or more types for wrapped function args to match. Keyword Arguments: raise_error: (bool, optional): If ``True`` then a ``TypeError`` will be raised if a validation fails. If ``False`` then an attribute will be set on decorated function named ``is_types_valid`` indicating if validation status. Default ``True``. type_instance_check (bool, optional): If ``True`` then args are tested also for ``isinstance()`` if type does not match, rather then just type check. If ``False`` then values willl only be tested as type. Default ``True`` ftype (DecFuncType, optional): Type of function that decorator is applied on. Default ``DecFuncType.FUNCTION`` opt_return (object, optional): Return value when decorator is invalid. By default an error is rasied when validation fails. If ``opt_return`` is supplied then it will be return when validation fails and no error will be raised. opt_args_filter (DecArgEnum, optional): Filters the arguments that are validated. Default ``DecArgEnum.ALL``. opt_logger (Union[Logger, LoggerAdapter], optional): Logger that logs exceptions when validation fails. Raises: TypeError: If ``types`` arg is not a iterable object such as a list or tuple. TypeError: If any arg is not of a type listed in ``types``. """ super().__init__(**kwargs) self._tc = None self._types = [arg for arg in args] if kwargs: # keyword args are passed to TypeChecker self._kwargs = {**kwargs} else: self._kwargs = {} self._opt_args_filter = DecArgEnum( kwargs.get("opt_args_filter", DecArgEnum.All_ARGS))
def __call__(self, func: callable): super()._call_init(func=func) @functools.wraps(func) def wrapper(*args, **kwargs): self._wrapper_init(args=args, kwargs=kwargs) arg_name_values = self._get_filtered_args_dict( self._opt_args_filter) try: is_valid = self._typechecker.validate(**arg_name_values) if self._typechecker.raise_error is False: wrapper.is_types_valid = is_valid if is_valid is False and self._is_opt_return() is True: return self._opt_return except TypeError as e: if self._is_opt_return(): return self._opt_return msg = str(e) msg = msg + self._get_class_dec_err() ex = TypeError(msg) self._log_err(ex) raise ex return func(*args, **kwargs) if self._typechecker.raise_error is False: wrapper.is_types_valid = True return wrapper @property def _typechecker(self) -> TypeChecker: if self._tc is None: self._tc = TypeChecker(*self._types, **self._kwargs) return self._tc
[docs]class AcceptedTypes(_DecBase): """ Decorator that decorates methods that requires args to match types specificed in a list See Also: :doc:`../../usage/Decorator/AcceptedTypes` """
[docs] def __init__(self, *args: Union[type, Iterable[type]], **kwargs): """ Constructor Other Parameters: args (Union[type, Iterable[type]]): One or more types or Iterator[type] for validation. Keyword Arguments: type_instance_check (bool, optional): If ``True`` then args are tested also for ``isinstance()`` if type does not match, rather then just type check. If ``False`` then values willl only be tested as type. Default ``True`` ftype (DecFuncType, optional): Type of function that decorator is applied on. Default ``DecFuncType.FUNCTION`` opt_return (object, optional): Return value when decorator is invalid. By default an error is rasied when validation fails. If ``opt_return`` is supplied then it will be return when validation fails and no error will be raised. opt_all_args (bool, optional): If ``True`` then the last subclass type passed into constructor will define any remaining args. This allows for one subclass to define required match of all arguments that decorator is applied to. Default ``False`` opt_args_filter (DecArgEnum, optional): Filters the arguments that are validated. Default ``DecArgEnum.ALL``. opt_logger (Union[Logger, LoggerAdapter], optional): Logger that logs exceptions when validation fails. """ super().__init__(**kwargs) self._tc = None self._types = [] ex_iterable_types = (Enum, str) for arg in args: if is_iterable(arg=arg, excluded_types=ex_iterable_types): arg_set = set() for arg_itm in arg: arg_set.add(arg_itm) self._types.append(arg_set) else: self._types.append(tuple([arg])) if kwargs: # keyword args are passed to TypeChecker self._kwargs = {**kwargs} else: self._kwargs = {} self._all_args = bool(kwargs.get("opt_all_args", False)) self._opt_args_filter = DecArgEnum( kwargs.get("opt_args_filter", DecArgEnum.All_ARGS))
def _get_formated_types(self, types: Union[Tuple[type], Set[type]]) -> str: # multi is list of set, actually one set in a list # single is a tuple of a single type. # these types are set in constructor. if isinstance(types, tuple): return f"'{types[0].__name__}'" lst_multi = [t.__name__ for t in types] result = Formatter.get_formated_names(names=lst_multi, conj='or') return result def _get_inst(self, types: Iterable[type]): return TypeChecker(*types, **self._kwargs) def _validate(self, func: callable, key: str, value: object, types: Iterable[type], arg_index: int, inst: SubClassChecker = None): if inst is None: tc = self._get_inst(types=types) else: tc = inst if Formatter.is_star_num(name=key): try: tc.validate(value) except TypeError: if self._is_opt_return(): return self._opt_return ex = TypeError(self._get_err_msg(name=None, value=value, types=types, arg_index=arg_index, fn=func)) self._log_err(err=ex) raise ex else: try: tc.validate(**{key: value}) except TypeError: if self._is_opt_return(): return self._opt_return ex = TypeError(self._get_err_msg(name=key, value=value, types=types, arg_index=arg_index, fn=func)) self._log_err(err=ex) raise ex return NO_THING def __call__(self, func: callable): super()._call_init(func=func) @functools.wraps(func) def wrapper(*args, **kwargs): self._wrapper_init(args=args, kwargs=kwargs) arg_name_values = self._get_filtered_args_dict( self._opt_args_filter) arg_keys = list(arg_name_values.keys()) arg_keys_len = arg_keys.__len__() i = 0 if arg_keys_len is not len(self._types): if self._all_args is False: if self._is_opt_return(): return self._opt_return msg = 'Invalid number of arguments for {0}()'.format( func.__name__) msg = msg + self._get_class_dec_err() ex = ValueError(msg) self._log_err(err=ex) raise ex arg_type = zip(arg_keys, self._types) for arg_info in arg_type: key = arg_info[0] result = self._validate(func=func, key=key, value=arg_name_values[key], types=arg_info[1], arg_index=i) if not result is NO_THING: return result i += 1 if arg_keys_len > i: # this only happens when _all_args is True # at this point remain args should match last last type in self._types r_args = arg_keys[i:] types = self._types[len(self._types) - 1] # tuple or set sc = self._get_inst(types=types) for r_arg in r_args: result = self._validate(func=func, key=r_arg, value=arg_name_values[r_arg], types=types, arg_index=i, inst=sc) if not result is NO_THING: return result i += 1 return func(*args, **kwargs) return wrapper def _get_err_msg(self, name: Union[str, None], value: object, types: Iterator[type], arg_index: int, fn: callable): str_types = self._get_formated_types(types=types) str_ord = Formatter.get_ordinal(arg_index + 1) if self._ftype == DecFuncEnum.PROPERTY_CLASS: msg = f"'{fn.__name__}' property error. Arg '{name}' expected type of {str_types} but got '{type(value).__name__}'." return msg if name: msg = f"Arg '{name}' in {str_ord} position is expected to be of {str_types} but got '{type(value).__name__}'." else: msg = f"Arg in {str_ord} position of is expected to be of {str_types} but got '{type(value).__name__}'." msg = msg + self._get_class_dec_err() return msg
[docs]class ArgsLen(_DecBase): """ Decorartor that sets the number of args that can be added to a function Raises: ValueError: If wrong args are passed into construcor. ValueError: If validation of arg count fails. See Also: :doc:`../../usage/Decorator/ArgsLen` """
[docs] def __init__(self, *args: Union[type, Iterable[type]], **kwargs): """ Constructor Other Parameters: args (Union[int, iterable[int]]): One or more int or Iterator[int] for validation. * Single ``int`` values are to match exact. * ``iterable[int]`` must be a pair of ``int`` with the first ``int`` less then the second ``int``. Keyword Arguments: ftype (DecFuncType, optional): Type of function that decorator is applied on. Default ``DecFuncType.FUNCTION`` opt_return (object, optional): Return value when decorator is invalid. By default an error is rasied when validation fails. If ``opt_return`` is supplied then it will be return when validation fails and no error will be raised. opt_logger (Union[Logger, LoggerAdapter], optional): Logger that logs exceptions when validation fails. """ super().__init__(**kwargs) self._ranges: Set[Tuple[int, int]] = set() self._lengths: Set[int] = set() for arg in args: if isinstance(arg, int): if arg >= 0: self._lengths.add(arg) elif is_iterable(arg) and len(arg) == 2: arg1 = arg[0] arg2 = arg[1] if isinstance(arg1, int) and isinstance(arg2, int) \ and arg1 >= 0 and arg2 > arg1: self._ranges.add((arg1, arg2)) valid = len(self._lengths) > 0 or len(self._ranges) > 0 if not valid: msg = f"{self.__class__.__name__} error. constructor must have valid args of of postive int and/or postive pairs of int." msg = msg + self._get_class_dec_err() raise ValueError(msg)
def _get_valid_counts(self) -> str: str_len = "" str_rng = "" len_lengths = len(self._lengths) len_ranges = len(self._ranges) if len_lengths > 0: str_len = Formatter.get_formated_names( names=sorted(self._lengths), conj='or') if len_ranges > 0: str_rng = Formatter.get_formated_names( names=sorted(self._ranges), conj='or', wrapper="") result = "" if len_lengths > 0: if len_lengths == 1: result = result + "Expected Length: " else: result = result + "Expected Lengths: " result = result + f"{str_len}." if len_ranges > 0: if len_lengths > 0: result = result + " " if len_ranges == 1: result = result + "Expected Range: " else: result = result + "Expected Ranges: " result = result + str_rng + "." return result def __call__(self, func: callable): super()._call_init(func=func) @functools.wraps(func) def wrapper(*args, **kwargs): self._wrapper_init(args=args, kwargs=kwargs) _args = self._get_args_star() _args_len = len(_args) is_valid = False if _args_len >= 0: for i in self._lengths: if _args_len == i: is_valid = True break if is_valid is False: for range in self._ranges: if _args_len >= range[0] and _args_len <= range[1]: is_valid = True break if is_valid is False: if self._is_opt_return(): return self._opt_return msg = f"Invalid number of args pass into '{func.__name__}'.\n{self._get_valid_counts()}" msg = msg + f" Got '{_args_len}' args." msg = msg + self._get_class_dec_err() ex = ValueError(msg) self._log_err(err=ex) raise ex return func(*args, **kwargs) # wrapper.is_types_valid = self.is_valid return wrapper
[docs]class ArgsMinMax(_DecBase): """ Decorartor that sets the min and or max number of args that can be added to a function See Also: :doc:`../../usage/Decorator/ArgsMinMax` """
[docs] def __init__(self, min: Optional[int] = 0, max: Optional[int] = None, **kwargs): """ Constructor Args: min (int, optional): Min number of args for a function. Defaults to 0. max (int, optional): Max number of args for a function. Defaults to None. Keyword Arguments: ftype (DecFuncType, optional): Type of function that decorator is applied on. Default ``DecFuncType.FUNCTION`` opt_return (object, optional): Return value when decorator is invalid. By default an error is rasied when validation fails. If ``opt_return`` is supplied then it will be return when validation fails and no error will be raised. opt_logger (Union[Logger, LoggerAdapter], optional): Logger that logs exceptions when validation fails. """ super().__init__(**kwargs) self._min = int(min) if isinstance(max, int): self._max = max else: self._max = None
def _get_min_max(self) -> Tuple[int, int]: _max = -1 if self._max is None else self._max _min = self._min return _min, _max def _get_valid_counts(self) -> str: _min, _max = self._get_min_max() msg = "" if _min > 0: msg = msg + "Expected min of '" + str(_min) + "'." if _max >= 0: if _min > 0: msg = msg + " " msg = msg + "Expected max of '" + str(_max) + "'." return msg def _get_error_msg(self, args_len: int) -> str: msg = f"Invalid number of args pass into '{self.fn.__name__}'.\n{self._get_valid_counts()}" msg = msg + f" Got '{args_len}' args." msg = msg + self._get_class_dec_err() return msg def __call__(self, func: callable): super()._call_init(func=func) @functools.wraps(func) def wrapper(*args, **kwargs): self._wrapper_init(args=args, kwargs=kwargs) _min, _max = self._get_min_max() has_rules = _min > 0 or _max >= 0 if has_rules: _args = self._get_args_star() _args_len = len(_args) is_valid = True if _min > 0: if _args_len < _min: is_valid = False if is_valid == True and _max >= 0: if _args_len > _max: is_valid = False if is_valid is False: if self._is_opt_return(): return self._opt_return ex = ValueError(self._get_error_msg(args_len=_args_len)) self._log_err(err=ex) raise ex return func(*args, **kwargs) # wrapper.is_types_valid = self.is_valid return wrapper
[docs]class ReturnRuleAll(_RuleBase): """ Decorator that decorates methods that require return value to match all rules specificed. See Also: :doc:`../../usage/Decorator/ReturnRuleAll` """
[docs] def __init__(self, *args: IRule, **kwargs): """ Constructor Args: args (IRule): One or more rules to use for validation Keyword Arguments: ftype (DecFuncType, optional): Type of function that decorator is applied on. Default ``DecFuncType.FUNCTION`` Default ``True`` opt_return (object, optional): Return value when decorator is invalid. By default an error is rasied when validation fails. If ``opt_return`` is supplied then it will be return when validation fails and no error will be raised. opt_logger (Union[Logger, LoggerAdapter], optional): Logger that logs exceptions when validation fails. """ super().__init__(**kwargs) self._rc = None self._rules = [arg for arg in args] if kwargs: self._kwargs = {**kwargs} else: self._kwargs = {}
def __call__(self, func: callable): super()._call_init(func=func) @functools.wraps(func) def wrapper(*args, **kwargs): self._wrapper_init(args=args, kwargs=kwargs) return_value = func(*args, **kwargs) rc = self._rulechecker try: rc.validate_all(**{"return": return_value}) except RuleError as e: if self._is_opt_return(): return self._opt_return err = self._get_err(e=e) self._log_err(err=err) raise err return return_value return wrapper @property def _rulechecker(self) -> RuleChecker: if self._rc is None: self._rc = RuleChecker(rules_all=self._rules, **self._kwargs) self._rc.raise_error = True return self._rc
[docs]class ReturnRuleAny(_RuleBase): """ Decorator that decorates methods that require return value to match any of the rules specificed. See Also: :doc:`../../usage/Decorator/ReturnRuleAny` """
[docs] def __init__(self, *args: IRule, **kwargs): """ Constructor Args: args (IRule): One or more rules to use for validation Keyword Arguments: ftype (DecFuncType, optional): Type of function that decorator is applied on. Default ``DecFuncType.FUNCTION`` opt_return (object, optional): Return value when decorator is invalid. By default an error is rasied when validation fails. If ``opt_return`` is supplied then it will be return when validation fails and no error will be raised. opt_logger (Union[Logger, LoggerAdapter], optional): Logger that logs exceptions when validation fails. """ super().__init__(**kwargs) self._rc = None self._rules = [arg for arg in args] if kwargs: self._kwargs = {**kwargs} else: self._kwargs = {}
def __call__(self, func: callable): super()._call_init(func=func) @functools.wraps(func) def wrapper(*args, **kwargs): self._wrapper_init(args=args, kwargs=kwargs) return_value = func(*args, **kwargs) rc = self._rulechecker # rc.current_arg = "return" try: rc.validate_any(**{"return": return_value}) except RuleError as e: if self._is_opt_return(): return self._opt_return err = self._get_err(e=e) self._log_err(err=err) raise err return return_value return wrapper @property def _rulechecker(self) -> RuleChecker: if self._rc is None: self._rc = RuleChecker(rules_any=self._rules, **self._kwargs) self._rc.raise_error = True return self._rc
[docs]class ReturnType(_DecBase): """ Decorator that decorates methods that require return value to match a type specificed. See Also: :doc:`../../usage/Decorator/ReturnType` """
[docs] def __init__(self, *args: type, **kwargs): """ Constructor Args: args (type): One ore more types that is used to validate return type. Keyword Arguments: type_instance_check (bool, optional): If ``True`` then args are tested also for ``isinstance()`` if type does not match, rather then just type check. If ``False`` then values willl only be tested as type. Default ``True`` opt_return (object, optional): Return value when decorator is invalid. By default an error is rasied when validation fails. If ``opt_return`` is supplied then it will be return when validation fails and no error will be raised. opt_logger (Union[Logger, LoggerAdapter], optional): Logger that logs exceptions when validation fails. """ super().__init__(**kwargs) self._tc = None self._types = [*args] if kwargs: # keyword args are passed to TypeChecker self._kwargs = {**kwargs} else: self._kwargs = {}
def __call__(self, func: callable): super()._call_init(func=func) @functools.wraps(func) def wrapper(*args, **kwargs): self._wrapper_init(args=args, kwargs=kwargs) return_value = func(*args, **kwargs) try: self._typechecker.validate(return_value) except TypeError: if self._is_opt_return(): return self._opt_return # catch type error and raise a new one so a more fitting message is raised. ex = TypeError(self._get_err_msg(return_value)) self._log_err(err=ex) raise ex return return_value return wrapper def _get_err_msg(self, value: object): str_types = self._get_formated_types(self._types, conj='or') msg = f"Return Value is expected to be of {str_types} but got '{type(value).__name__}'." msg = msg + self._get_class_dec_err() return msg @property def _typechecker(self) -> TypeChecker: if self._tc is None: self._tc = TypeChecker(*self._types, **self._kwargs) # ensure errors are raised if not valid self._tc.raise_error = True return self._tc
[docs]class TypeCheckKw(_DecBase): """ Decorator that decorates methods that require key, value args to match a type specificed in a list See Also: :doc:`../../usage/Decorator/TypeCheckKw` """
[docs] def __init__(self, arg_info: Dict[str, Union[int, type, Iterable[type]]], types: Optional[Iterable[Union[type, Iterable[type]]]] = None, **kwargs): """ Constructor Args: arg_info (Dict[str, Union[int, type, Iterable[type]]]): Dictionary of Key and int, type, or Iterable[type]. Each Key represents that name of an arg to match one or more types(s). If value is int then value is an index that corresponds to an item in ``types``. types (Iterable[Union[type, Iterable[type]]], optional): List of types for arg_info entries to match. Default ``None`` Keyword Arguments: raise_error: (bool, optional): If ``True`` then a ``TypeError`` will be raised if a validation fails. If ``False`` then an attribute will be set on decorated function named ``is_types_kw_valid`` indicating if validation status. Default ``True``. type_instance_check (bool, optional): If ``True`` then args are tested also for ``isinstance()`` if type does not match, rather then just type check. If ``False`` then values willl only be tested as type. Default ``True`` ftype (DecFuncType, optional): Type of function that decorator is applied on. Default ``DecFuncType.FUNCTION`` opt_return (object, optional): Return value when decorator is invalid. By default an error is rasied when validation fails. If ``opt_return`` is supplied then it will be return when validation fails and no error will be raised. opt_logger (Union[Logger, LoggerAdapter], optional): Logger that logs exceptions when validation fails. """ super().__init__(**kwargs) self._raise_error = bool(kwargs.get("raise_error", True)) self._arg_index = arg_info if types is None: self._types = [] else: self._types = types if kwargs: self._kwargs = {**kwargs} else: self._kwargs = {}
def _get_types(self, key: str) -> Iterable: value = self._arg_index[key] if isinstance(value, int): t = self._types[value] if isinstance(t, Iterable): return t return [t] if is_iterable(value): return value else: # make iterable return (value,) def __call__(self, func): super()._call_init(func=func) @functools.wraps(func) def wrapper(*args, **kwargs): self._wrapper_init(args=args, kwargs=kwargs) is_valid = True arg_name_values = self._get_args_dict() arg_keys = arg_name_values.keys() tc = False for key in self._arg_index.keys(): if key in arg_keys: is_valid = False types = self._get_types(key=key) if len(types) == 0: continue value = arg_name_values[key] tc = TypeChecker(*types, **self._kwargs) try: is_valid = tc.validate(**{key: value}) if is_valid is False: break except TypeError as e: if self._is_opt_return(): return self._opt_return msg = str(e) msg = msg + self._get_class_dec_err() ex = TypeError(msg) self._log_err(err=ex) raise ex if tc and tc.raise_error is False: wrapper.is_types_kw_valid = is_valid if is_valid == False and self._is_opt_return() == True: return self._opt_return return func(*args, **kwargs) if self._raise_error is False: wrapper.is_types_kw_valid = True return wrapper
[docs]class RuleCheckAny(_RuleBase): """ Decorator that decorates methods that require args to match a rule specificed in ``rules`` list. If a function arg does not match at least one rule in ``rules`` list then validation will fail. See Also: :doc:`../../usage/Decorator/RuleCheckAny` """
[docs] def __init__(self, *args: IRule, **kwargs): """ Constructor Other Parameters: args (IRule): One or more rules to use for validation Keyword Arguments: raise_error (bool, optional): If ``True`` then an Exception will be raised if a validation fails. The kind of exception raised depends on the rule that is invalid. Typically a ``TypeError`` or a ``ValueError`` is raised. If ``False`` then an attribute will be set on decorated function named ``is_rules_any_valid`` indicating if validation status. Default ``True``. ftype (DecFuncType, optional): Type of function that decorator is applied on. Default ``DecFuncType.FUNCTION`` opt_return (object, optional): Return value when decorator is invalid. By default an error is rasied when validation fails. If ``opt_return`` is supplied then it will be return when validation fails and no error will be raised. opt_args_filter (DecArgEnum, optional): Filters the arguments that are validated. Default ``DecArgEnum.ALL``. opt_logger (Union[Logger, LoggerAdapter], optional): Logger that logs exceptions when validation fails. """ super().__init__(**kwargs) self._raise_error = bool(kwargs.get("raise_error", True)) self._rc = None self._rules = [arg for arg in args] if kwargs: self._kwargs = {**kwargs} else: self._kwargs = {} self._opt_args_filter = DecArgEnum( kwargs.get("opt_args_filter", DecArgEnum.All_ARGS))
def __call__(self, func): super()._call_init(func=func) @functools.wraps(func) def wrapper(*args, **kwargs): self._wrapper_init(args=args, kwargs=kwargs) arg_name_values = self._get_filtered_args_dict( self._opt_args_filter) is_valid = False try: is_valid = self._rulechecker.validate_any(**arg_name_values) except RuleError as err: if self._is_opt_return(): return self._opt_return err_rule = self._get_err(e=err) self._log_err(err=err_rule) raise err_rule if self._raise_error is False: wrapper.is_rules_any_valid = is_valid if is_valid == False and self._is_opt_return() == True: return self._opt_return return func(*args, **kwargs) if self._raise_error is False: wrapper.is_rules_any_valid = True return wrapper @property def _rulechecker(self) -> RuleChecker: if self._rc is None: self._rc = RuleChecker(rules_any=self._rules, **self._kwargs) return self._rc
[docs]class RuleCheckAll(_RuleBase): """ Decorator that decorates methods that require args to match all rules specificed in ``rules`` list. If a function arg does not match all rules in ``rules`` list then validation will fail. See Also: :doc:`../../usage/Decorator/RuleCheckAll` """
[docs] def __init__(self, *args: IRule, **kwargs): """ Constructor Other Parameters: args (IRule): One or more rules to use for validation Keyword Arguments: raise_error (bool, optional): If ``True`` then an Exception will be raised if a validation fails. The kind of exception raised depends on the rule that is invalid. Typically a ``TypeError`` or a ``ValueError`` is raised. If ``False`` then an attribute will be set on decorated function named ``is_rules_all_valid`` indicating if validation status. Default ``True``. ftype (DecFuncType, optional): Type of function that decorator is applied on. Default ``DecFuncType.FUNCTION`` opt_return (object, optional): Return value when decorator is invalid. By default an error is rasied when validation fails. If ``opt_return`` is supplied then it will be return when validation fails and no error will be raised. opt_args_filter (DecArgEnum, optional): Filters the arguments that are validated. Default ``DecArgEnum.ALL``. opt_logger (Union[Logger, LoggerAdapter], optional): Logger that logs exceptions when validation fails. """ super().__init__(**kwargs) self._raise_error = bool(kwargs.get("raise_error", True)) self._rc = None self._rules = [arg for arg in args] if kwargs: self._kwargs = {**kwargs} else: self._kwargs = {} self._opt_args_filter = DecArgEnum( kwargs.get("opt_args_filter", DecArgEnum.All_ARGS))
def __call__(self, func): super()._call_init(func=func) @functools.wraps(func) def wrapper(*args, **kwargs): self._wrapper_init(args=args, kwargs=kwargs) arg_name_values = self._get_filtered_args_dict( self._opt_args_filter) is_valid = False try: is_valid = self._rulechecker.validate_all(**arg_name_values) except RuleError as err: if self._is_opt_return(): return self._opt_return err_rule = self._get_err(e=err) self._log_err(err=err_rule) raise err_rule if self._rulechecker.raise_error is False: wrapper.is_rules_all_valid = is_valid if is_valid == False and self._is_opt_return() == True: return self._opt_return return func(*args, **kwargs) if self._raise_error is False: wrapper.is_rules_all_valid = True return wrapper @property def _rulechecker(self) -> RuleChecker: if self._rc is None: self._rc = RuleChecker(rules_all=self._rules, **self._kwargs) return self._rc
[docs]class RuleCheckAllKw(_RuleBase): """ Decorator that decorates methods that require specific args to match rules specificed in ``rules`` list. If a function specific args do not match all matching rules in ``rules`` list then validation will fail. See Also: :doc:`../../usage/Decorator/RuleCheckAllKw` """
[docs] def __init__(self, arg_info: Dict[str, Union[int, IRule, Iterable[IRule]]], rules: Optional[Iterable[Union[IRule, Iterable[IRule]]]] = None, **kwargs): """ Constructor Args: arg_info (Dict[str, Union[int, IRule, Iterable[IRule]]]): Dictionary of Key and int, IRule, or Iterable[IRule]. Each Key represents that name of an arg to check with one or more rules. If value is int then value is an index that corresponds to an item in ``rules``. rules (Iterable[Union[IRule, Iterable[IRule]]], optional): List of rules for arg_info entries to match. Default ``None`` Keyword Arguments: raise_error (bool, optional): If ``True`` then an Exception will be raised if a validation fails. The kind of exception raised depends on the rule that is invalid. Typically a ``TypeError`` or a ``ValueError`` is raised. If ``False`` then an attribute will be set on decorated function named ``is_rules_kw_all_valid`` indicating if validation status. Default ``True``. ftype (DecFuncType, optional): Type of function that decorator is applied on. Default ``DecFuncType.FUNCTION`` opt_return (object, optional): Return value when decorator is invalid. By default an error is rasied when validation fails. If ``opt_return`` is supplied then it will be return when validation fails and no error will be raised. opt_logger (Union[Logger, LoggerAdapter], optional): Logger that logs exceptions when validation fails. """ super().__init__(**kwargs) self._raise_error = bool(kwargs.get("raise_error", True)) self._arg_index = arg_info if rules is None: self._rules = [] else: self._rules = rules if kwargs: self._kwargs = {**kwargs} else: self._kwargs = {}
def _get_rules(self, key: str) -> Iterable: value = self._arg_index[key] if isinstance(value, int): r = self._rules[value] if isinstance(r, Iterable): return r return [r] if isclass(value) and issubclass(value, IRule): return (value,) return value def __call__(self, func): super()._call_init(func=func) @functools.wraps(func) def wrapper(*args, **kwargs): self._wrapper_init(args=args, kwargs=kwargs) is_valid = True arg_name_values = self._get_args_dict() arg_keys = arg_name_values.keys() add_attrib = None for key in self._arg_index.keys(): if key in arg_keys: rules = self._get_rules(key=key) if len(rules) == 0: continue value = arg_name_values[key] rc = RuleChecker(rules_all=rules, **self._kwargs) if add_attrib is None: add_attrib = not rc.raise_error is_valid = False try: is_valid = rc.validate_all(**{key: value}) except RuleError as err: if self._is_opt_return(): return self._opt_return err_rule = self._get_err(e=err) self._log_err(err=err_rule) raise err_rule if is_valid is False: break if add_attrib: wrapper.is_rules_kw_all_valid = is_valid if is_valid == False and self._is_opt_return() == True: return self._opt_return return func(*args, **kwargs) if self._raise_error is False: wrapper.is_rules_kw_all_valid = True return wrapper
[docs]class RuleCheckAnyKw(RuleCheckAllKw): """ Decorator that decorates methods that require specific args to match rules specificed in ``rules`` list. If a function specific args do not match at least one matching rule in ``rules`` list then validation will fail. See Also: :doc:`../../usage/Decorator/RuleCheckAnyKw` """ def __call__(self, func): super()._call_init(func=func) @functools.wraps(func) def wrapper(*args, **kwargs): self._wrapper_init(args=args, kwargs=kwargs) is_valid = True arg_name_values = self._get_args_dict() arg_keys = arg_name_values.keys() add_attrib = None for key in self._arg_index.keys(): if key in arg_keys: rules = self._get_rules(key=key) if len(rules) == 0: continue value = arg_name_values[key] rc = RuleChecker(rules_any=rules, **self._kwargs) if add_attrib is None: add_attrib = not self._raise_error is_valid = False try: is_valid = rc.validate_any(**{key: value}) except RuleError as err: if self._is_opt_return(): return self._opt_return err_rule = self._get_err(e=err) self._log_err(err=err_rule) raise err_rule if is_valid is False: break if add_attrib: wrapper.is_rules_any_valid = is_valid if is_valid == False and self._is_opt_return() == True: return self._opt_return return func(*args, **kwargs) if self._raise_error is False: wrapper.is_rules_any_valid = True return wrapper
[docs]class RequireArgs(_DecBase): """ Decorator that defines required args for ``**kwargs`` of a function. See Also: :doc:`../../usage/Decorator/RequireArgs` """
[docs] def __init__(self, *args: str, **kwargs): """ Constructor Other Parameters: args (type): One or more names of wrapped function args to require. Keyword Arguments: ftype (DecFuncType, optional): Type of function that decorator is applied on. Default ``DecFuncType.FUNCTION`` opt_return (object, optional): Return value when decorator is invalid. By default an error is rasied when validation fails. If ``opt_return`` is supplied then it will be return when validation fails and no error will be raised. opt_logger (Union[Logger, LoggerAdapter], optional): Logger that logs exceptions when validation fails. """ super().__init__(**kwargs) self._args = [] for arg in args: if isinstance(arg, str): self._args.append(arg)
def __call__(self, func): super()._call_init(func=func) @functools.wraps(func) def wrapper(*args, **kwargs): self._wrapper_init(args=args, kwargs=kwargs) arg_name_values = self._get_args_dict() arg_keys = arg_name_values.keys() for key in self._args: if not key in arg_keys: if self._is_opt_return(): return self._opt_return ex = ValueError( f"'{func.__name__}', '{key}' is a required arg.") self._log_err(err=ex) raise ex return func(*args, **kwargs) return wrapper
[docs]class DefaultArgs(_CommonBase): """ Decorator that defines default values for ``**kwargs`` of a function. See Also: :doc:`../../usage/Decorator/DefaultArgs` """
[docs] def __init__(self, **kwargs: Dict[str, object]): """ Constructor Keyword Arguments: kwargs (Dict[str, object]): One or more Key, Value pairs to assign to wrapped function args as defaults. """ super().__init__(**kwargs) self._kwargs = {**kwargs}
def __call__(self, func): super()._call_init(func=func) @functools.wraps(func) def wrapper(*args, **kwargs): for key, value in self._kwargs.items(): if not key in kwargs: kwargs[key] = value return func(*args, **kwargs) return wrapper
def calltracker(func): """ Decorator method that adds ``has_been_called`` attribute to decorated method. ``has_been_called`` is ``False`` if method has not been called. ``has_been_called`` is ``True`` if method has been called. Note: This decorator needs to be the topmost decorator applied to a method Example: .. code-block:: python >>> @calltracker >>> def foo(msg): >>> print(msg) >>> print(foo.has_been_called) False >>> foo("Hello World") Hello World >>> print(foo.has_been_called) True See Also: :doc:`../../usage/Decorator/calltracker` """ @functools.wraps(func) def wrapper(*args, **kwargs): wrapper.has_been_called = True return func(*args, **kwargs) wrapper.has_been_called = False return wrapper def callcounter(func): """ Decorator method that adds ``call_count`` attribute to decorated method. ``call_count`` is ``0`` if method has not been called. ``call_count`` increases by 1 each time method is been called. Note: This decorator needs to be the topmost decorator applied to a method Example: .. code-block:: python >>> @callcounter >>> def foo(msg): >>> print(msg) >>> print("Call Count:", foo.call_count) 0 >>> foo("Hello") Hello >>> print("Call Count:", foo.call_count) 1 >>> foo("World") World >>> print("Call Count:", foo.call_count) 2 See Also: :doc:`../../usage/Decorator/callcounter` """ @functools.wraps(func) def wrapper(*args, **kwargs): wrapper.call_count += 1 return func(*args, **kwargs) wrapper.call_count = 0 return wrapper def singleton(orig_cls): """ Decorator that makes a class a singleton class Example: .. code-block:: python @singleton class Logger: def log(self, msg): print(msg) logger1 = Logger() logger2 = Logger() assert logger1 is logger See Also: :doc:`../../usage/Decorator/singleton` """ orig_new = orig_cls.__new__ instance = None @functools.wraps(orig_cls.__new__) def __new__(cls, *args, **kwargs): nonlocal instance if instance is None: instance = orig_new(cls, *args, **kwargs) return instance orig_cls.__new__ = __new__ return orig_cls
[docs]class AutoFill: """ Class decorator that replaces the ``__init__`` function with one that sets instance attributes with the specified argument names and default values. The original ``__init__`` is called with no arguments after the instance attributes have been assigned. Example: .. code-block:: python >>> @AutoFill('a', 'b', c=3) ... class Foo: pass >>> sorted(Foo(1, 2).__dict__.items()) [('a', 1), ('b', 2), ('c', 3)] """ # https://codereview.stackexchange.com/questions/142073/class-decorator-in-python-to-set-variables-for-the-constructor def __init__(self, *args, **kwargs): self._args = args self._kwargs = kwargs def __call__(self, cls): class Wrapped(cls): """Wrapped Class""" self._init(Wrapped) return Wrapped def _init(self, cls): argnames = self._args defaults = self._kwargs kind = Parameter.POSITIONAL_OR_KEYWORD signature = Signature( [Parameter(a, kind) for a in argnames] + [Parameter(k, kind, default=v) for k, v in defaults.items()]) original_init = cls.__init__ def init(self, *args, **kwargs): bound = signature.bind(*args, **kwargs) bound.apply_defaults() for k, v in bound.arguments.items(): setattr(self, k, v) original_init(self) cls.__init__ = init
[docs]class AutoFillKw: """ Class decorator that replaces the ``__init__`` function with one that sets instance attributes with the specified key, value of ``kwargs``. The original ``__init__`` is called with any ``*args`` after the instance attributes have been assigned. Example: .. code-block:: python >>> @AutoFillKw ... class Foo: pass >>> sorted(Foo(a=1, b=2, End="!").__dict__.items()) [('End', '!'), ('a', 1), ('b', 2)] """ def __init__(self, cls): self._cls = cls def __call__(self, *args, **kwargs): kind = Parameter.KEYWORD_ONLY signature = Signature( [Parameter(k, kind, default=v) for k, v in kwargs.items()]) original_init = self._cls.__init__ def init(self, *arguments, **kw): bound = signature.bind(**kw) bound.apply_defaults() for k, v in bound.arguments.items(): setattr(self, k, v) original_init(self, *arguments) self._cls.__init__ = init return self._cls(*args, **kwargs)
[docs]class SubClass(_DecBase): """ Decorator that requires args of a function to match or be a subclass of types specificed in constructor. See Also: :doc:`../../usage/Decorator/SubClass` """
[docs] def __init__(self, *args: Union[type, Iterable[type]], **kwargs): """ Constructor Other Parameters: args (Union[type, Iterable[type]]): One or more types or Iterator[type] for validation. Keyword Arguments: type_instance_check (bool, optional): If ``True`` then args are tested also for ``isinstance()`` if type does not match, rather then just type check. If ``False`` then values willl only be tested as type. Default ``True`` ftype (DecFuncType, optional): Type of function that decorator is applied on. Default ``DecFuncType.FUNCTION`` opt_return (object, optional): Return value when decorator is invalid. By default an error is rasied when validation fails. If ``opt_return`` is supplied then it will be return when validation fails and no error will be raised. opt_inst_only (bool, optional): If ``True`` then validation will requires all values being tested to be an instance of a class. If ``False`` valadition will test class instance and class type. Default ``True`` opt_all_args (bool, optional): If ``True`` then the last subclass type passed into constructor will define any remaining args. This allows for one subclass to define required match of all arguments that decorator is applied to. Default ``False`` opt_args_filter (DecArgEnum, optional): Filters the arguments that are validated. Default ``DecArgEnum.ALL``. opt_logger (Union[Logger, LoggerAdapter], optional): Logger that logs exceptions when validation fails. """ super().__init__(**kwargs) self._types = [] ex_iterable_types = (Enum, str) for arg in args: if is_iterable(arg=arg, excluded_types=ex_iterable_types): arg_set = set() for arg_itm in arg: arg_set.add(arg_itm) self._types.append(arg_set) else: self._types.append(tuple([arg])) if kwargs: # keyword args are passed to TypeChecker self._kwargs = {**kwargs} else: self._kwargs = {} self._all_args = bool(kwargs.get("opt_all_args", False)) self._opt_args_filter = DecArgEnum( kwargs.get("opt_args_filter", DecArgEnum.All_ARGS))
def _get_formated_types(self, types: Union[Tuple[type], Set[type]]) -> str: # multi is list of set, actually one set in a list # single is a tuple of a single type. # these types are set in constructor. if isinstance(types, tuple): return f"'{types[0].__name__}'" lst_multi = [t.__name__ for t in types] result = Formatter.get_formated_names(names=lst_multi, conj='or') return result def _get_inst(self, types: Iterable[type]): return SubClassChecker(*types, **self._kwargs) def _validate(self, key: str, value: object, types: Iterable[type], arg_index: int, inst: SubClassChecker = None): if inst is None: sc = self._get_inst(types=types) else: sc = inst # ensure errors are raised if not valid sc.raise_error = True if Formatter.is_star_num(name=key): try: sc.validate(value) except TypeError: if self._is_opt_return(): return self._opt_return ex = TypeError(self._get_err_msg(name=None, value=value, types=types, arg_index=arg_index)) self._log_err(err=ex) raise ex else: try: sc.validate(**{key: value}) except TypeError: if self._is_opt_return(): return self._opt_return ex = TypeError(self._get_err_msg(name=key, value=value, types=types, arg_index=arg_index)) self._log_err(err=ex) raise ex return NO_THING def __call__(self, func: callable): super()._call_init(func=func) @functools.wraps(func) def wrapper(*args, **kwargs): self._wrapper_init(args=args, kwargs=kwargs) arg_name_values = self._get_filtered_args_dict( self._opt_args_filter) arg_keys = list(arg_name_values.keys()) arg_keys_len = arg_keys.__len__() if self._all_args is False: if arg_keys_len is not len(self._types): if self._is_opt_return(): return self._opt_return msg = 'Invalid number of arguments for {0}()'.format( func.__name__) msg = msg + self._get_class_dec_err() ex = ValueError(msg) self._log_err(err=ex) raise ex arg_type = zip(arg_keys, self._types) i = 0 for arg_info in arg_type: key = arg_info[0] result = self._validate(key=key, value=arg_name_values[key], types=arg_info[1], arg_index=i) if not result is NO_THING: return result i += 1 if arg_keys_len > i: # this only happens when _all_args is True # at this point remain args should match last last type in self._types r_args = arg_keys[i:] types = self._types[len(self._types) - 1] # tuple or set sc = self._get_inst(types=types) for r_arg in r_args: result = self._validate(key=r_arg, value=arg_name_values[r_arg], types=types, arg_index=i, inst=sc) if not result is NO_THING: return result i += 1 return func(*args, **kwargs) return wrapper def _get_err_msg(self, name: Union[str, None], value: object, types: Iterator[type], arg_index: int): str_types = self._get_formated_types(types=types) str_ord = Formatter.get_ordinal(arg_index + 1) if self._ftype == DecFuncEnum.PROPERTY_CLASS: msg = f"'{self.fn.__name__}' property error. Arg '{name}' expected is expected be a subclass of {str_types}." return msg if name: msg = f"Arg '{name}' is expected be a subclass of {str_types}." else: msg = f"Arg in {str_ord} position is expected to be of a subclass of {str_types}." msg = msg + self._get_class_dec_err() return msg
[docs]class SubClasskKw(_DecBase): """ Decorator that requires args of a function to match or be a subclass of types specificed in constructor. See Also: :doc:`../../usage/Decorator/SubClasskKw` """
[docs] def __init__(self, arg_info: Dict[str, Union[int, type, Iterable[type]]], types: Optional[Iterable[Union[type, Iterable[type]]]] = None, **kwargs): """ Constructor Args: arg_info (Dict[str, Union[int, type, Iterable[type]]]): Dictionary of Key and int, type, or Iterable[type]. Each Key represents that name of an arg to match one or more types(s). If value is int then value is an index that corresponds to an item in ``types``. types (Iterable[Union[type, Iterable[type]]], optional): List of types for arg_info entries to match. Default ``None`` Keyword Arguments: type_instance_check (bool, optional): If ``True`` then args are tested also for ``isinstance()`` if type does not match, rather then just type check. If ``False`` then values willl only be tested as type. Default ``True`` ftype (DecFuncType, optional): Type of function that decorator is applied on. Default ``DecFuncType.FUNCTION`` opt_return (object, optional): Return value when decorator is invalid. By default an error is rasied when validation fails. If ``opt_return`` is supplied then it will be return when validation fails and no error will be raised. opt_logger (Union[Logger, LoggerAdapter], optional): Logger that logs exceptions when validation fails. """ super().__init__(**kwargs) self._arg_index = arg_info if types is None: self._types = [] else: self._types = types if kwargs: self._kwargs = {**kwargs} else: self._kwargs = {} # set rais_error for SubClassChecker as this class does not support this option. self._kwargs['raise_error'] = True
def _get_types(self, key: str) -> Iterable: value = self._arg_index[key] if isinstance(value, int): t = self._types[value] if isinstance(t, Iterable): return t return [t] if is_iterable(value): return value else: # make iterable return (value,) def __call__(self, func): super()._call_init(func=func) @functools.wraps(func) def wrapper(*args, **kwargs): self._wrapper_init(args=args, kwargs=kwargs) arg_name_values = self._get_args_dict() arg_keys = arg_name_values.keys() sc = False for key in self._arg_index.keys(): if key in arg_keys: types = self._get_types(key=key) if len(types) == 0: continue value = arg_name_values[key] sc = SubClassChecker(*types, **self._kwargs) try: # error_raise is always True for sc. # for this reason no need to capture results of validate. sc.validate(**{key: value}) except TypeError as e: if self._is_opt_return(): return self._opt_return msg = str(e) msg = msg + self._get_class_dec_err() ex = TypeError(msg) self._log_err(err=ex) raise ex return func(*args, **kwargs) return wrapper