diff --git a/logwrap/__init__.pxd b/logwrap/__init__.pxd index 802ae15..aa09ac5 100644 --- a/logwrap/__init__.pxd +++ b/logwrap/__init__.pxd @@ -1 +1 @@ -cdef tuple __all__ +cpdef tuple __all__ diff --git a/logwrap/__init__.pyx b/logwrap/__init__.pyx index e57c6c6..765d677 100644 --- a/logwrap/__init__.pyx +++ b/logwrap/__init__.pyx @@ -1,10 +1,9 @@ -from .repr_utils cimport PrettyFormat, PrettyRepr, PrettyStr -from .repr_utils import pretty_repr, pretty_str +from .repr_utils cimport PrettyFormat, PrettyRepr, PrettyStr, pretty_repr, pretty_str from .log_wrap cimport LogWrap from .log_wrap import logwrap, BoundParameter, bind_args_kwargs -cdef tuple __all__ = ( +cpdef tuple __all__ = ( "LogWrap", "logwrap", "PrettyFormat", diff --git a/logwrap/class_decorator.pxd b/logwrap/class_decorator.pxd index 318b0d5..76e3f77 100644 --- a/logwrap/class_decorator.pxd +++ b/logwrap/class_decorator.pxd @@ -16,5 +16,6 @@ cdef class BaseDecorator: - cdef readonly object _func - cdef dict __dict__ + cdef: + readonly object _func + dict __dict__ diff --git a/logwrap/class_decorator.py b/logwrap/class_decorator.py index 1659b24..62fcb02 100644 --- a/logwrap/class_decorator.py +++ b/logwrap/class_decorator.py @@ -26,9 +26,7 @@ class BaseDecorator(metaclass=abc.ABCMeta): Implements wrapping and __call__, wrapper getter is abstract. - Note: - wrapper getter is called only on function call, - if decorator used without braces. + .. note:: wrapper getter is called only on function call, if decorator used without braces. Usage example: diff --git a/logwrap/class_decorator.pyx b/logwrap/class_decorator.pyx index ca0c81e..b65da63 100644 --- a/logwrap/class_decorator.pyx +++ b/logwrap/class_decorator.pyx @@ -19,10 +19,19 @@ import typing cdef class BaseDecorator: - """Base class for decorators.""" + """Base class for decorators. + + Implements wrapping and __call__, wrapper getter is abstract. + + .. note:: wrapper getter is called only on function call, if decorator used without braces. + """ def __init__(self, func: typing.Optional[typing.Callable] = None) -> None: - """Decorator.""" + """Decorator. + + :param func: function to wrap + :type func: typing.Optional[typing.Callable] + """ # noinspection PyArgumentList super(BaseDecorator, self).__init__() # pylint: disable=assigning-non-slot @@ -32,7 +41,12 @@ cdef class BaseDecorator: # pylint: enable=assigning-non-slot def _get_function_wrapper(self, func: typing.Callable) -> typing.Callable: - """Here should be constructed and returned real decorator.""" + """Here should be constructed and returned real decorator. + + :param func: Wrapped function + :type func: typing.Callable + :rtype: typing.Callable + """ raise NotImplementedError() # pragma: no cover def __call__(self, *args: typing.Union[typing.Callable, typing.Any], **kwargs: typing.Any) -> typing.Any: diff --git a/logwrap/log_wrap.pxd b/logwrap/log_wrap.pxd index 5308c21..4fa5029 100644 --- a/logwrap/log_wrap.pxd +++ b/logwrap/log_wrap.pxd @@ -20,38 +20,34 @@ import typing from logwrap cimport class_decorator -cdef unsigned int indent +cdef: + unsigned int indent -cdef str pretty_repr( - src: typing.Any, - unsigned int indent=?, - bint no_indent_start=?, - unsigned int max_indent=?, - unsigned int indent_step=? -) + class LogWrap(class_decorator.BaseDecorator): + """Base class for LogWrap implementation.""" + cdef: + public unsigned int log_level + public unsigned int exc_level + public unsigned int max_indent -cdef class LogWrap(class_decorator.BaseDecorator): - """Base class for LogWrap implementation.""" + public bint log_call_args + public bint log_call_args_on_exc + public bint log_traceback + public bint log_result_obj - cdef public unsigned int log_level - cdef public unsigned int exc_level - cdef public unsigned int max_indent + readonly object _spec + readonly object _logger - cdef public bint log_call_args - cdef public bint log_call_args_on_exc - cdef public bint log_traceback - cdef public bint log_result_obj + list __blacklisted_names + list __blacklisted_exceptions - cdef list __blacklisted_names - cdef list __blacklisted_exceptions - cdef object __logger - cdef object __spec + cpdef object pre_process_param(self, object arg) + cpdef str post_process_param(self, object arg, str arg_repr) - cdef str _get_func_args_repr( - self, sig: inspect.Signature, args: typing.Tuple, kwargs: typing.Dict[str, typing.Any] - ) - cdef void _make_done_record(self, str func_name, result: typing.Any) - cdef void _make_calling_record(self, str name, str arguments, str method=?) - cdef void _make_exc_record(self, str name, str arguments) + cdef: + str _get_func_args_repr(self, sig: inspect.Signature, tuple args, dict kwargs) + void _make_done_record(self, str func_name, result: typing.Any) + void _make_calling_record(self, str name, str arguments, str method=?) + void _make_exc_record(self, str name, str arguments) diff --git a/logwrap/log_wrap.py b/logwrap/log_wrap.py index 21869d2..17ee61a 100644 --- a/logwrap/log_wrap.py +++ b/logwrap/log_wrap.py @@ -712,7 +712,7 @@ def logwrap( # noqa: F811 # pylint: disable=unexpected-keyword-arg, no-value-f log_traceback: bool = True, log_result_obj: bool = True ) -> typing.Union[LogWrap, typing.Callable]: - """Log function calls and return values. Python 3.4+ version. + """Log function calls and return values. :param func: function to wrap :type func: typing.Optional[typing.Callable] diff --git a/logwrap/log_wrap.pyx b/logwrap/log_wrap.pyx index eda0d9f..2133266 100644 --- a/logwrap/log_wrap.pyx +++ b/logwrap/log_wrap.pyx @@ -26,7 +26,7 @@ from logwrap cimport repr_utils from logwrap cimport class_decorator -cdef tuple __all__ = ("LogWrap", "logwrap", "BoundParameter", "bind_args_kwargs") +cpdef tuple __all__ = ("LogWrap", "logwrap", "BoundParameter", "bind_args_kwargs") logger = logging.getLogger("logwrap") # type: logging.Logger @@ -36,19 +36,6 @@ fmt = "\n{spc:<{indent}}{{key!r}}={{val}},{{annotation}}".format(spc="", indent= comment = "\n{spc:<{indent}}# {{kind!s}}:".format(spc="", indent=indent).format -cdef str pretty_repr( - src: typing.Any, - unsigned int indent=0, - bint no_indent_start=False, - unsigned int max_indent=20, - unsigned int indent_step=4 -): - """Make human readable repr of object.""" - return repr_utils.PrettyRepr(max_indent=max_indent, indent_step=indent_step)( - src=src, indent=indent, no_indent_start=no_indent_start - ) - - class BoundParameter: """Parameter-like object store BOUND with value parameter.""" @@ -63,7 +50,14 @@ class BoundParameter: empty = inspect.Parameter.empty def __init__(self, parameter: inspect.Parameter, value: typing.Any = inspect.Parameter.empty) -> None: - """Parameter-like object store BOUND with value parameter.""" + """Parameter-like object store BOUND with value parameter. + + :param parameter: parameter from signature + :type parameter: inspect.Parameter + :param value: parameter real value + :type value: typing.Any + :raises ValueError: No default value and no value + """ self._parameter = parameter if value is self.empty: @@ -105,7 +99,10 @@ class BoundParameter: # noinspection PyTypeChecker def __hash__(self) -> int: - """Block hashing.""" + """Block hashing. + + :raises TypeError: Not hashable. + """ msg = "unhashable type: '{0}'".format(self.__class__.__name__) raise TypeError(msg) @@ -150,7 +147,19 @@ class BoundParameter: def bind_args_kwargs( sig: inspect.Signature, *args: typing.Any, **kwargs: typing.Any ) -> typing.Iterator[BoundParameter]: - """Bind *args and **kwargs to signature and get Bound Parameters.""" + """Bind *args and **kwargs to signature and get Bound Parameters. + + :param sig: source signature + :type sig: inspect.Signature + :param args: not keyworded arguments + :type args: typing.Any + :param kwargs: keyworded arguments + :type kwargs: typing.Any + :return: Iterator for bound parameters with all information about it + :rtype: typing.Iterator[BoundParameter] + + .. versionadded:: 3.3.0 + """ bound = sig.bind(*args, **kwargs).arguments parameters = list(sig.parameters.values()) for param in parameters: @@ -176,7 +185,42 @@ cdef class LogWrap(class_decorator.BaseDecorator): bint log_traceback=True, bint log_result_obj=True ) -> None: - """Log function calls and return values.""" + """Log function calls and return values. + + :param func: function to wrap + :type func: typing.Optional[typing.Callable] + :param log: logger object for decorator, by default used 'logwrap' + :type log: logging.Logger + :param log_level: log level for successful calls + :type log_level: int + :param exc_level: log level for exception cases + :type exc_level: int + :param max_indent: maximum indent before classic `repr()` call. + :type max_indent: int + :param spec: callable object used as spec for arguments bind. + This is designed for the special cases only, + when impossible to change signature of target object, + but processed/redirected signature is accessible. + Note: this object should provide fully compatible + signature with decorated function, or arguments bind + will be failed! + :type spec: typing.Optional[typing.Callable] + :param blacklisted_names: Blacklisted argument names. Arguments with this names will be skipped in log. + :type blacklisted_names: typing.Optional[typing.Iterable[str]] + :param blacklisted_exceptions: list of exception, which should be re-raised without producing log record. + :type blacklisted_exceptions: typing.Optional[typing.Iterable[typing.Type[Exception]]] + :param log_call_args: log call arguments before executing wrapped function. + :type log_call_args: bool + :param log_call_args_on_exc: log call arguments if exception raised. + :type log_call_args_on_exc: bool + :param log_traceback: log traceback on excaption in addition to failure info + :type log_traceback: bool + :param log_result_obj: log result of function call. + :type log_result_obj: bool + + .. versionchanged:: 3.3.0 Extract func from log and do not use Union. + .. versionchanged:: 5.1.0 log_traceback parameter + """ super(LogWrap, self).__init__(func=func) self.log_level = log_level @@ -198,9 +242,8 @@ cdef class LogWrap(class_decorator.BaseDecorator): else: self.__blacklisted_exceptions = list(blacklisted_exceptions) - self.__logger = log - - self.__spec = spec or self._func + self._logger = log + self._spec = spec or self._func # We are not interested to pass any arguments to object @@ -214,16 +257,6 @@ cdef class LogWrap(class_decorator.BaseDecorator): """List of exceptions to re-raise without log.""" return self.__blacklisted_exceptions - @property - def _logger(self) -> logging.Logger: - """Logger instance.""" - return self.__logger - - @property - def _spec(self) -> typing.Optional[typing.Callable]: - """Spec for function arguments.""" - return self.__spec - def __repr__(self) -> str: """Repr for debug purposes.""" return ( @@ -240,111 +273,164 @@ cdef class LogWrap(class_decorator.BaseDecorator): "log_result_obj={self.log_result_obj}, )".format(cls=self.__class__.__name__, self=self, spec=self._spec) ) - def pre_process_param( - self, arg: BoundParameter - ) -> typing.Union[BoundParameter, typing.Tuple[BoundParameter, typing.Any], None]: - """Process parameter for the future logging.""" + cpdef object pre_process_param(self, object arg: BoundParameter): + """Process parameter for the future logging. + + :param arg: bound parameter + :type arg: BoundParameter + :return: value, value override for logging or None if argument should not be logged. + :rtype: typing.Union[BoundParameter, typing.Tuple[BoundParameter, typing.Any], None] + + Override this method if some modifications required for parameter value before logging + + .. versionadded:: 3.3.0 + """ return arg - def post_process_param(self, arg: BoundParameter, str arg_repr: str) -> str: - """Process parameter for the future logging.""" - return arg_repr + cpdef str post_process_param(self, object arg: BoundParameter, str arg_repr): + """Process parameter for the future logging. + + :param arg: bound parameter + :type arg: BoundParameter + :param arg_repr: repr for value + :type arg_repr: str + :return: processed repr for value + :rtype: str + + Override this method if some modifications required for result of repr() over parameter - cdef str _get_func_args_repr(self, sig: inspect.Signature, args: typing.Tuple, kwargs: typing.Dict[str, typing.Any]): - """Internal helper for reducing complexity of decorator code.""" - if not (self.log_call_args or self.log_call_args_on_exc): - return "" - - cdef str param_str = "" - cdef str val - cdef str annotation - - last_kind = None - for param in bind_args_kwargs(sig, *args, **kwargs): - if param.name in self.blacklisted_names: - continue - - preprocessed = self.pre_process_param(param) - if preprocessed is None: - continue - - if isinstance(preprocessed, (tuple, list)): - param, value = preprocessed - else: - value = param.value - - if param.empty is value: - if param.VAR_POSITIONAL == param.kind: - value = () - elif param.VAR_KEYWORD == param.kind: - value = {} - - val = pretty_repr(src=value, indent=indent + 4, no_indent_start=True, max_indent=self.max_indent) - - val = self.post_process_param(param, val) - - if last_kind != param.kind: - param_str += comment(kind=param.kind) - last_kind = param.kind - - if param.empty is param.annotation: - annotation = "" - else: - annotation = " # type: {param.annotation!s}".format(param=param) - - param_str += fmt(key=param.name, annotation=annotation, val=val) - if param_str: - param_str += "\n" - return param_str - - cdef void _make_done_record(self, str func_name, result: typing.Any): - """Construct success record.""" - cdef str msg = "Done: {name!r}".format(name=func_name) - - if self.log_result_obj: - msg += " with result:\n{result}".format( - result=pretty_repr( - result, - indent=0, - no_indent_start=False, - max_indent=self.max_indent, - indent_step=4 + .. versionadded:: 3.3.0 + """ + return arg_repr + cdef: + str _get_func_args_repr(self, sig: inspect.Signature, tuple args, dict kwargs): + """Internal helper for reducing complexity of decorator code. + + :param sig: function signature + :type sig: inspect.Signature + :param args: not keyworded arguments + :type args: typing.Tuple + :param kwargs: keyworded arguments + :type kwargs: typing.Dict[str, typing.Any] + :return: repr over function arguments + :rtype: str + + .. versionchanged:: 3.3.0 Use pre- and post- processing of params during execution + """ + if not (self.log_call_args or self.log_call_args_on_exc): + return "" + + cdef: + str param_str = "" + str val + str annotation + + last_kind = None + for param in bind_args_kwargs(sig, *args, **kwargs): + if param.name in self.blacklisted_names: + continue + + preprocessed = self.pre_process_param(param) + if preprocessed is None: + continue + + if isinstance(preprocessed, (tuple, list)): + param, value = preprocessed + else: + value = param.value + + if param.empty is value: + if param.VAR_POSITIONAL == param.kind: + value = () + elif param.VAR_KEYWORD == param.kind: + value = {} + + val = repr_utils.pretty_repr(src=value, indent=indent + 4, no_indent_start=True, max_indent=self.max_indent) + + val = self.post_process_param(param, val) + + if last_kind != param.kind: + param_str += comment(kind=param.kind) + last_kind = param.kind + + if param.empty is param.annotation: + annotation = "" + else: + annotation = " # type: {param.annotation!s}".format(param=param) + + param_str += fmt(key=param.name, annotation=annotation, val=val) + if param_str: + param_str += "\n" + return param_str + + void _make_done_record(self, str func_name, result: typing.Any): + """Construct success record. + + :type func_name: str + :type result: typing.Any + """ + cdef str msg = "Done: {name!r}".format(name=func_name) + + if self.log_result_obj: + msg += " with result:\n{result}".format( + result=repr_utils.pretty_repr( + result, + indent=0, + no_indent_start=False, + max_indent=self.max_indent, + indent_step=4 + + ) ) + self._logger.log(level=self.log_level, msg=msg) # type: ignore + + void _make_calling_record(self, str name, str arguments, str method="Calling"): + """Make log record before execution. + + :type name: str + :type arguments: str + :type method: str + """ + self._logger.log( # type: ignore + level=self.log_level, + msg="{method}: \n{name!r}({arguments})".format( + method=method, name=name, arguments=arguments if self.log_call_args else "" + ), ) - self._logger.log(level=self.log_level, msg=msg) # type: ignore - - cdef void _make_calling_record(self, str name, str arguments, str method="Calling"): - """Make log record before execution.""" - self._logger.log( # type: ignore - level=self.log_level, - msg="{method}: \n{name!r}({arguments})".format( - method=method, name=name, arguments=arguments if self.log_call_args else "" - ), - ) - cdef void _make_exc_record(self, str name, str arguments): - """Make log record if exception raised.""" - exc_info = sys.exc_info() - stack = traceback.extract_stack() - tb = traceback.extract_tb(exc_info[2]) - full_tb = stack[:2] + tb # cut decorator and build full traceback - exc_line = traceback.format_exception_only(*exc_info[:2]) - # Make standard traceback string - cdef str tb_text = "Traceback (most recent call last):\n" + "".join(traceback.format_list(full_tb)) + "".join(exc_line) - - self._logger.log( # type: ignore - level=self.exc_level, - msg="Failed: \n{name!r}({arguments})\n{tb_text}".format( - name=name, - arguments=arguments if self.log_call_args_on_exc else "", - tb_text=tb_text if self.log_traceback else "", - ), - exc_info=False, - ) + void _make_exc_record(self, str name, str arguments): + """Make log record if exception raised. + + :type name: str + :type arguments: str + """ + exc_info = sys.exc_info() + stack = traceback.extract_stack() + tb = traceback.extract_tb(exc_info[2]) + full_tb = stack[:2] + tb # cut decorator and build full traceback + exc_line = traceback.format_exception_only(*exc_info[:2]) + # Make standard traceback string + cdef str tb_text = "Traceback (most recent call last):\n" + "".join(traceback.format_list(full_tb)) + "".join(exc_line) + + self._logger.log( # type: ignore + level=self.exc_level, + msg="Failed: \n{name!r}({arguments})\n{tb_text}".format( + name=name, + arguments=arguments if self.log_call_args_on_exc else "", + tb_text=tb_text if self.log_traceback else "", + ), + exc_info=False, + ) def _get_function_wrapper(self, func: typing.Callable) -> typing.Callable: - """Here should be constructed and returned real decorator.""" + """Here should be constructed and returned real decorator. + + :param func: Wrapped function + :type func: typing.Callable + :return: wrapped coroutine or function + :rtype: typing.Callable + """ sig = inspect.signature(self._spec or func) @functools.wraps(func) @@ -402,7 +488,45 @@ def logwrap( bint log_traceback=True, bint log_result_obj=True ) -> typing.Union[LogWrap, typing.Callable]: - """Log function calls and return values.""" + """Log function calls and return values. + + :param func: function to wrap + :type func: typing.Optional[typing.Callable] + :param log: logger object for decorator, by default used 'logwrap' + :type log: logging.Logger + :param log_level: log level for successful calls + :type log_level: int + :param exc_level: log level for exception cases + :type exc_level: int + :param max_indent: maximum indent before classic `repr()` call. + :type max_indent: int + :param spec: callable object used as spec for arguments bind. + This is designed for the special cases only, + when impossible to change signature of target object, + but processed/redirected signature is accessible. + Note: this object should provide fully compatible signature + with decorated function, or arguments bind will be failed! + :type spec: typing.Optional[typing.Callable] + :param blacklisted_names: Blacklisted argument names. Arguments with this names will be skipped in log. + :type blacklisted_names: typing.Optional[typing.Iterable[str]] + :param blacklisted_exceptions: list of exceptions, which should be re-raised without producing log record. + :type blacklisted_exceptions: typing.Optional[typing.Iterable[typing.Type[Exception]]] + :param log_call_args: log call arguments before executing wrapped function. + :type log_call_args: bool + :param log_call_args_on_exc: log call arguments if exception raised. + :type log_call_args_on_exc: bool + :param log_traceback: log traceback on excaption in addition to failure info + :type log_traceback: bool + :param log_result_obj: log result of function call. + :type log_result_obj: bool + :return: built real decorator. + :rtype: _log_wrap_shared.BaseLogWrap + + .. versionchanged:: 3.3.0 Extract func from log and do not use Union. + .. versionchanged:: 3.3.0 Deprecation of *args + .. versionchanged:: 4.0.0 Drop of *args + .. versionchanged:: 5.1.0 log_traceback parameter + """ wrapper = LogWrap( log=log, log_level=log_level, diff --git a/logwrap/repr_utils.pxd b/logwrap/repr_utils.pxd index fefd06d..2a9d649 100644 --- a/logwrap/repr_utils.pxd +++ b/logwrap/repr_utils.pxd @@ -22,18 +22,41 @@ import types import typing -cdef class PrettyFormat: - cdef readonly unsigned int max_indent - cdef readonly unsigned int indent_step - cdef readonly str _magic_method_name - - cdef int next_indent(self, unsigned int indent, unsigned int multiplier=?) - cdef str _repr_callable(self, src: typing.Union[types.FunctionType, types.MethodType], unsigned int indent=?) - cdef str _repr_simple(self, src: typing.Any, unsigned int indent=?, bint no_indent_start=?) - cdef str _repr_iterable_item(self, bint nl, str obj_type, str prefix, unsigned int indent, str result, str suffix) - -cdef class PrettyRepr(PrettyFormat): - cdef str _strings_repr(self, unsigned int indent, val: typing.Union[bytes, str]) - -cdef class PrettyStr(PrettyFormat): - cdef str _strings_str(self, unsigned int indent, val: typing.Union[bytes, str]) +cdef: + class PrettyFormat: + cdef: + readonly unsigned int max_indent + readonly unsigned int indent_step + readonly str _magic_method_name + + cpdef int next_indent(self, unsigned int indent, unsigned int multiplier=?) + + cdef: + str _repr_callable(self, src: typing.Union[types.FunctionType, types.MethodType], unsigned int indent=?) + str _repr_simple(self, src: typing.Any, unsigned int indent=?, bint no_indent_start=?) + str _repr_iterable_item(self, bint nl, str obj_type, str prefix, unsigned int indent, str result, str suffix) + + cpdef str process_element(self, src: typing.Any, unsigned int indent=?, bint no_indent_start=?) + + class PrettyRepr(PrettyFormat): + cdef str _strings_repr(self, unsigned int indent, val: typing.Union[bytes, str]) + + class PrettyStr(PrettyFormat): + cdef str _strings_str(self, unsigned int indent, val: typing.Union[bytes, str]) + + +cpdef str pretty_repr( + src: typing.Any, + unsigned int indent=?, + bint no_indent_start=?, + unsigned int max_indent=?, + unsigned int indent_step=? +) + +cpdef str pretty_str( + src: typing.Any, + unsigned int indent=?, + bint no_indent_start=?, + unsigned int max_indent=?, + unsigned int indent_step=? +) diff --git a/logwrap/repr_utils.py b/logwrap/repr_utils.py index 870401a..a078ee0 100644 --- a/logwrap/repr_utils.py +++ b/logwrap/repr_utils.py @@ -26,6 +26,9 @@ import typing +__all__ = ("PrettyFormat", "PrettyRepr", "PrettyStr", "pretty_repr", "pretty_str") + + def _known_callable(item: typing.Any) -> bool: """Check for possibility to parse callable.""" return isinstance(item, (types.FunctionType, types.MethodType)) @@ -628,6 +631,3 @@ def pretty_str( return PrettyStr(max_indent=max_indent, indent_step=indent_step)( src=src, indent=indent, no_indent_start=no_indent_start ) - - -__all__ = ("PrettyFormat", "PrettyRepr", "PrettyStr", "pretty_repr", "pretty_str") diff --git a/logwrap/repr_utils.pyx b/logwrap/repr_utils.pyx index 1f5c918..46d9a10 100644 --- a/logwrap/repr_utils.pyx +++ b/logwrap/repr_utils.pyx @@ -23,17 +23,18 @@ import types import typing -cdef tuple __all__ = ("PrettyFormat", "PrettyRepr", "PrettyStr", "pretty_repr", "pretty_str") +cpdef tuple __all__ = ("PrettyFormat", "PrettyRepr", "PrettyStr", "pretty_repr", "pretty_str") -cdef bint _known_callable(item: typing.Any): - """Check for possibility to parse callable.""" - return isinstance(item, (types.FunctionType, types.MethodType)) +cdef: + bint _known_callable(item: typing.Any): + """Check for possibility to parse callable.""" + return isinstance(item, (types.FunctionType, types.MethodType)) -cdef bint _simple(item: typing.Any): - """Check for nested iterations: True, if not.""" - return not isinstance(item, (list, set, tuple, dict, frozenset)) + bint _simple(item: typing.Any): + """Check for nested iterations: True, if not.""" + return not isinstance(item, (list, set, tuple, dict, frozenset)) class ReprParameter: @@ -111,8 +112,13 @@ class ReprParameter: # pylint: disable=no-member def _prepare_repr(func: typing.Union[types.FunctionType, types.MethodType]) -> typing.Iterator[ReprParameter]: - """Get arguments lists with defaults.""" - ismethod = isinstance(func, types.MethodType) + """Get arguments lists with defaults. + + :param func: Callable object to process + :type func: typing.Union[types.FunctionType, types.MethodType] + :return: repr of callable parameter from signature + :rtype: typing.Iterator[ReprParameter]""" + cdef bint ismethod = isinstance(func, types.MethodType) if not ismethod: real_func = func else: @@ -140,40 +146,114 @@ cdef class PrettyFormat: """ def __cinit__(self, unsigned int max_indent=20, unsigned int indent_step=4): - """Pretty Formatter.""" + """Pretty Formatter. + + :param max_indent: maximal indent before classic repr() call + :type max_indent: int + :param indent_step: step for the next indentation level + :type indent_step: int + """ self.max_indent = max_indent self.indent_step = indent_step - cdef int next_indent(self, unsigned int indent, unsigned int multiplier=1): - """Next indentation value.""" - return indent + multiplier * self.indent_step + cpdef int next_indent(self, unsigned int indent, unsigned int multiplier=1): + """Next indentation value. - cdef str _repr_callable(self, src: typing.Union[types.FunctionType, types.MethodType], unsigned int indent=0): - """Repr callable object (function or method).""" - raise NotImplementedError() + :param indent: current indentation value + :type indent: int + :param multiplier: step multiplier + :type multiplier: int + :return: next indentation value + :rtype: int + """ + return indent + multiplier * self.indent_step - cdef str _repr_simple(self, src: typing.Any, unsigned int indent=0, bint no_indent_start=False): - """Repr object without iteration.""" - raise NotImplementedError() + cdef: + str _repr_callable(self, src: typing.Union[types.FunctionType, types.MethodType], unsigned int indent=0): + """Repr callable object (function or method). + + :param src: Callable to process + :type src: typing.Union[types.FunctionType, types.MethodType] + :param indent: start indentation + :type indent: int + :return: Repr of function or method with signature. + :rtype: str + """ + raise NotImplementedError() + + str _repr_simple(self, src: typing.Any, unsigned int indent=0, bint no_indent_start=False): + """Repr object without iteration. + + :param src: Source object + :type src: typing.Any + :param indent: start indentation + :type indent: int + :param no_indent_start: ignore indent + :type no_indent_start: bool + :return: simple repr() over object + :rtype: str + """ + raise NotImplementedError() + + str _repr_iterable_item(self, bint nl, str obj_type, str prefix, unsigned int indent, str result, str suffix): + """Repr iterable item. + + :param nl: newline before item + :type nl: bool + :param obj_type: Object type + :type obj_type: str + :param prefix: prefix + :type prefix: str + :param indent: start indentation + :type indent: int + :param result: result of pre-formatting + :type result: str + :param suffix: suffix + :type suffix: str + :rtype: str + """ + raise NotImplementedError() def _repr_dict_items(self, dict src, unsigned int indent=0) -> typing.Iterator[str]: # type - """Repr dict items.""" - raise NotImplementedError() + """Repr dict items. - cdef str _repr_iterable_item(self, bint nl, str obj_type, str prefix, unsigned int indent, str result, str suffix): - """Repr iterable item.""" + :param src: object to process + :type src: typing.Dict + :param indent: start indentation + :type indent: int + :rtype: typing.Iterator[str] + """ raise NotImplementedError() def _repr_iterable_items(self, src: typing.Iterable, unsigned int indent=0) -> typing.Iterator[str]: - """Repr iterable items (not designed for dicts).""" + """Repr iterable items (not designed for dicts). + + :param src: object to process + :type src: typing.Iterable + :param indent: start indentation + :type indent: int + :return: repr of element in iterable item + :rtype: typing.Iterator[str] + """ for elem in src: yield "\n" + self.process_element(src=elem, indent=self.next_indent(indent)) + "," - def process_element(self, src: typing.Any, unsigned int indent=0, bint no_indent_start=False) -> str: - """Make human readable representation of object.""" - cdef str prefix - cdef str suffix - cdef str result + cpdef str process_element(self, src: typing.Any, unsigned int indent=0, bint no_indent_start=False): + """Make human readable representation of object. + + :param src: object to process + :type src: typing.Any + :param indent: start indentation + :type indent: int + :param no_indent_start: do not indent open bracket and simple parameters + :type no_indent_start: bool + :return: formatted string + :rtype: str + """ + cdef: + str prefix + str suffix + str result if hasattr(src, self._magic_method_name): result = getattr(src, self._magic_method_name)(self, indent=indent, no_indent_start=no_indent_start) @@ -206,7 +286,17 @@ cdef class PrettyFormat: ) def __call__(self, src: typing.Any, unsigned int indent=0, bint no_indent_start=False) -> str: - """Make human readable representation of object. The main entry point.""" + """Make human readable representation of object. The main entry point. + + :param src: object to process + :type src: typing.Any + :param indent: start indentation + :type indent: int + :param no_indent_start: do not indent open bracket and simple parameters + :type no_indent_start: bool + :return: formatted string + :rtype: str + """ result = self.process_element(src, indent=indent, no_indent_start=no_indent_start) return result @@ -222,28 +312,116 @@ cdef class PrettyRepr(PrettyFormat): def __cinit__(self, unsigned int max_indent=20, unsigned int indent_step=4): self._magic_method_name = "__pretty_repr__" - cdef str _strings_repr(self, unsigned int indent, val: typing.Union[bytes, str]): - """Custom repr for strings and binary strings.""" - cdef str prefix + cdef: + str _strings_repr(self, unsigned int indent, val: typing.Union[bytes, str]): + """Custom repr for strings and binary strings.""" + cdef str prefix - if isinstance(val, bytes): - val = val.decode(encoding="utf-8", errors="backslashreplace") - prefix = "b" - else: - prefix = "u" - return "{spc:<{indent}}{prefix}'''{string}'''".format(spc="", indent=indent, prefix=prefix, string=val) - - cdef str _repr_simple(self, src: typing.Any, unsigned int indent=0, bint no_indent_start=False): - """Repr object without iteration.""" - indent = 0 if no_indent_start else indent - if isinstance(src, set): - return "{spc:<{indent}}{val}".format(spc="", indent=indent, val="set(" + " ,".join(map(repr, src)) + ")") - if isinstance(src, (bytes, str)): - return self._strings_repr(indent=indent, val=src) - return "{spc:<{indent}}{val!r}".format(spc="", indent=indent, val=src) + if isinstance(val, bytes): + val = val.decode(encoding="utf-8", errors="backslashreplace") + prefix = "b" + else: + prefix = "u" + return "{spc:<{indent}}{prefix}'''{string}'''".format(spc="", indent=indent, prefix=prefix, string=val) + + str _repr_simple(self, src: typing.Any, unsigned int indent=0, bint no_indent_start=False): + """Repr object without iteration. + + :param src: Source object + :type src: typing.Any + :param indent: start indentation + :type indent: int + :param no_indent_start: ignore indent + :type no_indent_start: bool + :return: simple repr() over object, except strings (add prefix) and set (uniform py2/py3) + :rtype: str + """ + indent = 0 if no_indent_start else indent + if isinstance(src, set): + return "{spc:<{indent}}{val}".format(spc="", indent=indent, val="set(" + " ,".join(map(repr, src)) + ")") + if isinstance(src, (bytes, str)): + return self._strings_repr(indent=indent, val=src) + return "{spc:<{indent}}{val!r}".format(spc="", indent=indent, val=src) + + str _repr_callable(self, src: typing.Union[types.FunctionType, types.MethodType], unsigned int indent=0): + """Repr callable object (function or method). + + :param src: Callable to process + :type src: typing.Union[types.FunctionType, types.MethodType] + :param indent: start indentation + :type indent: int + :return: Repr of function or method with signature. + :rtype: str + """ + cdef: + str param_str = "" + str annotation + + for param in _prepare_repr(src): + param_str += "\n{spc:<{indent}}{param.name}".format(spc="", indent=self.next_indent(indent), param=param) + if param.annotation is not param.empty: + param_str += ": {param.annotation}".format(param=param) + if param.value is not param.empty: + param_str += "={val}".format( + val=self.process_element(src=param.value, indent=indent, no_indent_start=True) + ) + param_str += "," + + if param_str: + param_str += "\n" + " " * indent + + sig = inspect.signature(src) + if sig.return_annotation is inspect.Parameter.empty: + annotation = "" + else: + annotation = " -> {sig.return_annotation!r}".format(sig=sig) + + return "\n{spc:<{indent}}<{obj!r} with interface ({args}){annotation}>".format( + spc="", indent=indent, obj=src, args=param_str, annotation=annotation + ) + + str _repr_iterable_item(self, bint nl, str obj_type, str prefix, unsigned int indent, str result, str suffix): + """Repr iterable item. + + :param nl: newline before item + :type nl: bool + :param obj_type: Object type + :type obj_type: str + :param prefix: prefix + :type prefix: str + :param indent: start indentation + :type indent: int + :param result: result of pre-formatting + :type result: str + :param suffix: suffix + :type suffix: str + :return: formatted repr of "result" with prefix and suffix to explain type. + :rtype: str + """ + return ( + "{nl}" + "{spc:<{indent}}{obj_type:}({prefix}{result}\n" + "{spc:<{indent}}{suffix})".format( + nl="\n" if nl else "", + spc="", + indent=indent, + obj_type=obj_type, + prefix=prefix, + result=result, + suffix=suffix, + ) + ) def _repr_dict_items(self, dict src, unsigned int indent=0) -> typing.Iterator[str]: - """Repr dict items.""" + """Repr dict items. + + :param src: object to process + :type src: dict + :param indent: start indentation + :type indent: int + :return: repr of key/value pair from dict + :rtype: typing.Iterator[str] + """ cdef unsigned int max_len = max((len(repr(key)) for key in src)) if src else 0 for key, val in src.items(): @@ -255,74 +433,115 @@ cdef class PrettyRepr(PrettyFormat): val=self.process_element(val, indent=self.next_indent(indent, multiplier=2), no_indent_start=True), ) - cdef str _repr_callable(self, src: typing.Union[types.FunctionType, types.MethodType], unsigned int indent=0): - """Repr callable object (function or method).""" - cdef str param_str = "" - cdef str annotation - - for param in _prepare_repr(src): - param_str += "\n{spc:<{indent}}{param.name}".format(spc="", indent=self.next_indent(indent), param=param) - if param.annotation is not param.empty: - param_str += ": {param.annotation}".format(param=param) - if param.value is not param.empty: - param_str += "={val}".format( - val=self.process_element(src=param.value, indent=indent, no_indent_start=True) - ) - param_str += "," - - if param_str: - param_str += "\n" + " " * indent - - sig = inspect.signature(src) - if sig.return_annotation is inspect.Parameter.empty: - annotation = "" - else: - annotation = " -> {sig.return_annotation!r}".format(sig=sig) - - return "\n{spc:<{indent}}<{obj!r} with interface ({args}){annotation}>".format( - spc="", indent=indent, obj=src, args=param_str, annotation=annotation - ) - - cdef str _repr_iterable_item(self, bint nl, str obj_type, str prefix, unsigned int indent, str result, str suffix): - """Repr iterable item.""" - return ( - "{nl}" - "{spc:<{indent}}{obj_type:}({prefix}{result}\n" - "{spc:<{indent}}{suffix})".format( - nl="\n" if nl else "", - spc="", - indent=indent, - obj_type=obj_type, - prefix=prefix, - result=result, - suffix=suffix, - ) - ) - cdef class PrettyStr(PrettyFormat): - """Pretty str.""" + """Pretty str. + + Designed for usage as __str__ replacement on complex objects + """ def __cinit__(self, unsigned int max_indent=20, unsigned int indent_step=4): self._magic_method_name = "__pretty_str__" - cdef str _strings_str(self, unsigned int indent, val: typing.Union[bytes, str]): - """Custom repr for strings and binary strings.""" - if isinstance(val, bytes): - val = val.decode(encoding="utf-8", errors="backslashreplace") - return "{spc:<{indent}}{string}".format(spc="", indent=indent, string=val) - - cdef str _repr_simple(self, src: typing.Any, unsigned int indent=0, bint no_indent_start=False): - """Repr object without iteration.""" - indent = 0 if no_indent_start else indent - if isinstance(src, set): - return "{spc:<{indent}}{val}".format(spc="", indent=indent, val="set(" + " ,".join(map(str, src)) + ")") - if isinstance(src, (bytes, str)): - return self._strings_str(indent=indent, val=src) - return "{spc:<{indent}}{val!s}".format(spc="", indent=indent, val=src) + cdef: + str _strings_str(self, unsigned int indent, val: typing.Union[bytes, str]): + """Custom repr for strings and binary strings.""" + if isinstance(val, bytes): + val = val.decode(encoding="utf-8", errors="backslashreplace") + return "{spc:<{indent}}{string}".format(spc="", indent=indent, string=val) + + str _repr_simple(self, src: typing.Any, unsigned int indent=0, bint no_indent_start=False): + """Repr object without iteration. + + :param src: Source object + :type src: typing.Any + :param indent: start indentation + :type indent: int + :param no_indent_start: ignore indent + :type no_indent_start: bool + :return: simple repr() over object, except strings (decode) and set (uniform py2/py3) + :rtype: str + """ + indent = 0 if no_indent_start else indent + if isinstance(src, set): + return "{spc:<{indent}}{val}".format(spc="", indent=indent, val="set(" + " ,".join(map(str, src)) + ")") + if isinstance(src, (bytes, str)): + return self._strings_str(indent=indent, val=src) + return "{spc:<{indent}}{val!s}".format(spc="", indent=indent, val=src) + + str _repr_callable(self, src: typing.Union[types.FunctionType, types.MethodType], unsigned int indent=0): + """Repr callable object (function or method). + + :param src: Callable to process + :type src: typing.Union[types.FunctionType, types.MethodType] + :param indent: start indentation + :type indent: int + :return: Repr of function or method with signature. + :rtype: str + """ + cdef: + str param_str = "" + str annotation + + for param in _prepare_repr(src): + param_str += "\n{spc:<{indent}}{param.name}".format(spc="", indent=self.next_indent(indent), param=param) + if param.annotation is not param.empty: + param_str += ": {param.annotation}".format(param=param) + if param.value is not param.empty: + param_str += "={val}".format( + val=self.process_element(src=param.value, indent=indent, no_indent_start=True) + ) + param_str += "," + + if param_str: + param_str += "\n" + " " * indent + + sig = inspect.signature(src) + if sig.return_annotation is inspect.Parameter.empty: + annotation = "" + else: + annotation = " -> {sig.return_annotation!r}".format(sig=sig) + + return "\n{spc:<{indent}}<{obj!s} with interface ({args}){annotation}>".format( + spc="", indent=indent, obj=src, args=param_str, annotation=annotation + ) + + str _repr_iterable_item(self, bint nl, str obj_type, str prefix, unsigned int indent, str result, str suffix): + """Repr iterable item. + + :param nl: newline before item + :type nl: bool + :param obj_type: Object type + :type obj_type: str + :param prefix: prefix + :type prefix: str + :param indent: start indentation + :type indent: int + :param result: result of pre-formatting + :type result: str + :param suffix: suffix + :type suffix: str + :return: formatted repr of "result" with prefix and suffix to explain type. + :rtype: str + """ + return ( + "{nl}" + "{spc:<{indent}}{prefix}{result}\n" + "{spc:<{indent}}{suffix}".format( + nl="\n" if nl else "", spc="", indent=indent, prefix=prefix, result=result, suffix=suffix + ) + ) def _repr_dict_items(self, dict src, unsigned int indent=0) -> typing.Iterator[str]: - """Repr dict items.""" + """Repr dict items. + + :param src: object to process + :type src: dict + :param indent: start indentation + :type indent: int + :return: repr of key/value pair from dict + :rtype: typing.Iterator[str] + """ cdef unsigned int max_len = max((len(str(key)) for key in src)) if src else 0 for key, val in src.items(): yield "\n{spc:<{indent}}{key!s:{size}}: {val},".format( @@ -333,66 +552,55 @@ cdef class PrettyStr(PrettyFormat): val=self.process_element(val, indent=self.next_indent(indent, multiplier=2), no_indent_start=True), ) - cdef str _repr_callable(self, src: typing.Union[types.FunctionType, types.MethodType], unsigned int indent=0): - """Repr callable object (function or method).""" - cdef str param_str = "" - cdef str annotation - - for param in _prepare_repr(src): - param_str += "\n{spc:<{indent}}{param.name}".format(spc="", indent=self.next_indent(indent), param=param) - if param.annotation is not param.empty: - param_str += ": {param.annotation}".format(param=param) - if param.value is not param.empty: - param_str += "={val}".format( - val=self.process_element(src=param.value, indent=indent, no_indent_start=True) - ) - param_str += "," - - if param_str: - param_str += "\n" + " " * indent - - sig = inspect.signature(src) - if sig.return_annotation is inspect.Parameter.empty: - annotation = "" - else: - annotation = " -> {sig.return_annotation!r}".format(sig=sig) - - return "\n{spc:<{indent}}<{obj!s} with interface ({args}){annotation}>".format( - spc="", indent=indent, obj=src, args=param_str, annotation=annotation - ) - cdef str _repr_iterable_item(self, bint nl, str obj_type, str prefix, unsigned int indent, str result, str suffix): - """Repr iterable item.""" - return ( - "{nl}" - "{spc:<{indent}}{prefix}{result}\n" - "{spc:<{indent}}{suffix}".format( - nl="\n" if nl else "", spc="", indent=indent, prefix=prefix, result=result, suffix=suffix - ) - ) - - -def pretty_repr( +cpdef str pretty_repr( src: typing.Any, unsigned int indent=0, bint no_indent_start=False, unsigned int max_indent=20, unsigned int indent_step=4 -) -> str: - """Make human readable repr of object.""" +): + """Make human readable repr of object. + + :param src: object to process + :type src: typing.Any + :param indent: start indentation, all next levels is +indent_step + :type indent: int + :param no_indent_start: do not indent open bracket and simple parameters + :type no_indent_start: bool + :param max_indent: maximal indent before classic repr() call + :type max_indent: int + :param indent_step: step for the next indentation level + :type indent_step: int + :return: formatted string + :rtype: str + """ return PrettyRepr(max_indent=max_indent, indent_step=indent_step)( src=src, indent=indent, no_indent_start=no_indent_start ) -def pretty_str( +cpdef str pretty_str( src: typing.Any, unsigned int indent=0, bint no_indent_start=False, unsigned int max_indent=20, unsigned int indent_step=4 -) -> str: - """Make human readable str of object.""" +): + """Make human readable str of object. + + :param src: object to process + :type src: typing.Any + :param indent: start indentation, all next levels is +indent_step + :type indent: int + :param no_indent_start: do not indent open bracket and simple parameters + :type no_indent_start: bool + :param max_indent: maximal indent before classic repr() call + :type max_indent: int + :param indent_step: step for the next indentation level + :type indent_step: int + :return: formatted string + """ return PrettyStr(max_indent=max_indent, indent_step=indent_step)( src=src, indent=indent, no_indent_start=no_indent_start )