From 8ba7b9a9c1423915881fa9451da7273c474490e2 Mon Sep 17 00:00:00 2001 From: Alexey Stepanov Date: Thu, 8 Nov 2018 10:50:31 +0100 Subject: [PATCH 1/2] Use pure cython: less magic to generate, less overhead (#51) make make internals less hidden (cherry picked from commit d278b11c252a83e9378c97e5c4d0468d92d0c56d) Signed-off-by: Alexey Stepanov --- MANIFEST.in | 1 + build_requirements.txt | 2 +- logwrap/__init__.py | 6 +- logwrap/__init__.pyx | 0 logwrap/class_decorator.pxd | 20 + ..._class_decorator.py => class_decorator.py} | 0 logwrap/class_decorator.pyx | 56 +++ logwrap/log_wrap.pxd | 57 +++ logwrap/{_log_wrap.py => log_wrap.py} | 4 +- logwrap/log_wrap.pyx | 417 ++++++++++++++++++ logwrap/repr_utils.pxd | 39 ++ logwrap/{_repr_utils.py => repr_utils.py} | 2 +- logwrap/repr_utils.pyx | 386 ++++++++++++++++ setup.py | 9 +- test/test_log_wrap_shared.py | 18 +- test/test_repr_utils.py | 34 +- 16 files changed, 1002 insertions(+), 49 deletions(-) create mode 100644 logwrap/__init__.pyx create mode 100644 logwrap/class_decorator.pxd rename logwrap/{_class_decorator.py => class_decorator.py} (100%) create mode 100644 logwrap/class_decorator.pyx create mode 100644 logwrap/log_wrap.pxd rename logwrap/{_log_wrap.py => log_wrap.py} (99%) create mode 100644 logwrap/log_wrap.pyx create mode 100644 logwrap/repr_utils.pxd rename logwrap/{_repr_utils.py => repr_utils.py} (99%) create mode 100644 logwrap/repr_utils.pyx diff --git a/MANIFEST.in b/MANIFEST.in index 69fb558..db0fdaa 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,5 @@ include *.rst LICENSE requirements.txt +global-include *.pyx *.pxd global-exclude *.c exclude Makefile prune tools diff --git a/build_requirements.txt b/build_requirements.txt index 21ba7bb..5afb0a7 100644 --- a/build_requirements.txt +++ b/build_requirements.txt @@ -1,4 +1,4 @@ Cython; platform_python_implementation == "CPython" -wheel +wheel<0.32.0 -r CI_REQUIREMENTS.txt -r requirements.txt diff --git a/logwrap/__init__.py b/logwrap/__init__.py index 0e18cde..51f5d08 100644 --- a/logwrap/__init__.py +++ b/logwrap/__init__.py @@ -22,8 +22,8 @@ later it has been reworked and extended for support of special cases. """ -from ._repr_utils import PrettyFormat, PrettyRepr, PrettyStr, pretty_repr, pretty_str -from ._log_wrap import logwrap, LogWrap, BoundParameter, bind_args_kwargs +from .repr_utils import PrettyFormat, PrettyRepr, PrettyStr, pretty_repr, pretty_str +from .log_wrap import logwrap, LogWrap, BoundParameter, bind_args_kwargs __all__ = ( "LogWrap", @@ -37,7 +37,7 @@ "bind_args_kwargs", ) -__version__ = "4.9.2" +__version__ = "4.9.3" __author__ = "Alexey Stepanov" __author_email__ = "penguinolog@gmail.com" __maintainers__ = { diff --git a/logwrap/__init__.pyx b/logwrap/__init__.pyx new file mode 100644 index 0000000..e69de29 diff --git a/logwrap/class_decorator.pxd b/logwrap/class_decorator.pxd new file mode 100644 index 0000000..318b0d5 --- /dev/null +++ b/logwrap/class_decorator.pxd @@ -0,0 +1,20 @@ +# Copyright 2017-2018 Alexey Stepanov aka penguinolog +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Base class for decorators.""" + + +cdef class BaseDecorator: + cdef readonly object _func + cdef dict __dict__ diff --git a/logwrap/_class_decorator.py b/logwrap/class_decorator.py similarity index 100% rename from logwrap/_class_decorator.py rename to logwrap/class_decorator.py diff --git a/logwrap/class_decorator.pyx b/logwrap/class_decorator.pyx new file mode 100644 index 0000000..6d0b851 --- /dev/null +++ b/logwrap/class_decorator.pyx @@ -0,0 +1,56 @@ +# Copyright 2017-2018 Alexey Stepanov aka penguinolog +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Base class for decorators.""" + +import functools +import typing + + +cdef class BaseDecorator: + """Base class for decorators.""" + + def __init__(self, func: typing.Optional[typing.Callable] = None) -> None: + """Decorator.""" + # noinspection PyArgumentList + super(BaseDecorator, self).__init__() + # pylint: disable=assigning-non-slot + self._func = func # type: typing.Optional[typing.Callable] + if self._func is not None: + functools.update_wrapper(self, self._func) + # pylint: enable=assigning-non-slot + + def _get_function_wrapper(self, func: typing.Callable) -> typing.Callable: + """Here should be constructed and returned real decorator.""" + raise NotImplementedError() # pragma: no cover + + def __call__(self, *args: typing.Union[typing.Callable, typing.Any], **kwargs: typing.Any) -> typing.Any: + """Main decorator getter.""" + l_args = list(args) + + if self._func: + wrapped = self._func # type: typing.Callable + else: + wrapped = l_args.pop(0) + + wrapper = self._get_function_wrapper(wrapped) + if self._func: + return wrapper(*l_args, **kwargs) + return wrapper + + def __repr__(self) -> str: + """For debug purposes.""" + return "<{cls}({func!r}) at 0x{id:X}>".format( + cls=self.__class__.__name__, func=self._func, id=id(self) + ) # pragma: no cover diff --git a/logwrap/log_wrap.pxd b/logwrap/log_wrap.pxd new file mode 100644 index 0000000..c624c5a --- /dev/null +++ b/logwrap/log_wrap.pxd @@ -0,0 +1,57 @@ +# Copyright 2016-2018 Alexey Stepanov aka penguinolog + +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""log_wrap shared code module.""" + +import inspect +import typing + +from . cimport class_decorator + + +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=? +) + + +cdef class LogWrap(class_decorator.BaseDecorator): + """Base class for LogWrap implementation.""" + + cdef public unsigned int log_level + cdef public unsigned int exc_level + cdef public unsigned int max_indent + + 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 + + cdef list __blacklisted_names + cdef list __blacklisted_exceptions + cdef object __logger + cdef object __spec + + 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) diff --git a/logwrap/_log_wrap.py b/logwrap/log_wrap.py similarity index 99% rename from logwrap/_log_wrap.py rename to logwrap/log_wrap.py index 517361d..21869d2 100644 --- a/logwrap/_log_wrap.py +++ b/logwrap/log_wrap.py @@ -25,7 +25,7 @@ import typing import logwrap as core -from . import _class_decorator +from . import class_decorator __all__ = ("LogWrap", "logwrap", "BoundParameter", "bind_args_kwargs") @@ -171,7 +171,7 @@ def bind_args_kwargs( # pylint: disable=assigning-non-slot,abstract-method # noinspection PyAbstractClass -class LogWrap(_class_decorator.BaseDecorator): +class LogWrap(class_decorator.BaseDecorator): """Base class for LogWrap implementation.""" __slots__ = ( diff --git a/logwrap/log_wrap.pyx b/logwrap/log_wrap.pyx new file mode 100644 index 0000000..4dc0a14 --- /dev/null +++ b/logwrap/log_wrap.pyx @@ -0,0 +1,417 @@ +# Copyright 2016-2018 Alexey Stepanov aka penguinolog + +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""log_wrap shared code module.""" + +import asyncio +import functools +import inspect +import logging +import sys +import traceback +import typing + +from . cimport repr_utils +from . cimport class_decorator + + +__all__ = ("LogWrap", "logwrap", "BoundParameter", "bind_args_kwargs") + +logger = logging.getLogger("logwrap") # type: logging.Logger + + +cdef unsigned int indent = 4 +fmt = "\n{spc:<{indent}}{{key!r}}={{val}},{{annotation}}".format(spc="", indent=indent).format +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.""" + + __slots__ = ("_parameter", "_value") + + POSITIONAL_ONLY = inspect.Parameter.POSITIONAL_ONLY + POSITIONAL_OR_KEYWORD = inspect.Parameter.POSITIONAL_OR_KEYWORD + VAR_POSITIONAL = inspect.Parameter.VAR_POSITIONAL + KEYWORD_ONLY = inspect.Parameter.KEYWORD_ONLY + VAR_KEYWORD = inspect.Parameter.VAR_KEYWORD + + 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.""" + self._parameter = parameter + + if value is self.empty: + if parameter.default is self.empty and parameter.kind not in (self.VAR_POSITIONAL, self.VAR_KEYWORD): + raise ValueError("Value is not set and no default value") + self._value = parameter.default + else: + self._value = value + + @property + def parameter(self) -> inspect.Parameter: + """Parameter object.""" + return self._parameter + + @property + def name(self) -> typing.Union[None, str]: + """Parameter name.""" + return self.parameter.name + + @property + def default(self) -> typing.Any: + """Parameter default value.""" + return self.parameter.default + + @property + def annotation(self) -> typing.Union[inspect.Parameter.empty, str]: + """Parameter annotation.""" + return self.parameter.annotation + + @property + def kind(self) -> int: + """Parameter kind.""" + return self.parameter.kind # type: ignore + + @property + def value(self) -> typing.Any: + """Parameter value.""" + return self._value + + # noinspection PyTypeChecker + def __hash__(self) -> int: + """Block hashing.""" + msg = "unhashable type: '{0}'".format(self.__class__.__name__) + raise TypeError(msg) + + def __str__(self) -> str: + """Debug purposes.""" + # POSITIONAL_ONLY is only in precompiled functions + if self.kind == self.POSITIONAL_ONLY: + as_str = "" if self.name is None else "<{as_str}>".format(as_str=self.name) + else: + as_str = self.name or "" + + # Add annotation if applicable (python 3 only) + if self.annotation is not self.empty: + as_str += ": {annotation!s}".format(annotation=inspect.formatannotation(self.annotation)) + + value = self.value + if self.empty is value: + if self.VAR_POSITIONAL == self.kind: + value = () + elif self.VAR_KEYWORD == self.kind: + value = {} + + as_str += "={value!r}".format(value=value) + + if self.default is not self.empty: + as_str += " # {self.default!r}".format(self=self) + + if self.kind == self.VAR_POSITIONAL: + as_str = "*" + as_str + elif self.kind == self.VAR_KEYWORD: + as_str = "**" + as_str + + return as_str + + def __repr__(self) -> str: + """Debug purposes.""" + return '<{} "{}">'.format(self.__class__.__name__, self) + + +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.""" + bound = sig.bind(*args, **kwargs).arguments + parameters = list(sig.parameters.values()) + for param in parameters: + yield BoundParameter(parameter=param, value=bound.get(param.name, param.default)) + + +cdef class LogWrap(class_decorator.BaseDecorator): + """Base class for LogWrap implementation.""" + + def __init__( + self, + func: typing.Optional[typing.Callable] = None, + *, + log: logging.Logger = logger, + unsigned int log_level=logging.DEBUG, + unsigned int exc_level=logging.ERROR, + unsigned int max_indent=20, + spec: typing.Optional[typing.Callable] = None, + blacklisted_names: typing.Optional[typing.Iterable[str]] = None, + blacklisted_exceptions: typing.Optional[typing.Iterable[typing.Type[Exception]]] = None, + bint log_call_args=True, + bint log_call_args_on_exc=True, + bint log_traceback=True, + bint log_result_obj=True + ) -> None: + """Log function calls and return values.""" + super(LogWrap, self).__init__(func=func) + + self.log_level = log_level + self.exc_level = exc_level + self.max_indent = max_indent + + self.log_call_args = log_call_args + self.log_call_args_on_exc = log_call_args_on_exc + self.log_traceback = log_traceback + self.log_result_obj = log_result_obj + + # Typing fix: + if blacklisted_names is None: + self.__blacklisted_names = [] # type: typing.List[str] + else: + self.__blacklisted_names = list(blacklisted_names) + if blacklisted_exceptions is None: + self.__blacklisted_exceptions = [] # type: typing.List[typing.Type[Exception]] + else: + self.__blacklisted_exceptions = list(blacklisted_exceptions) + + self.__logger = log + + self.__spec = spec or self._func + + # We are not interested to pass any arguments to object + + @property + def blacklisted_names(self) -> typing.List[str]: + """List of arguments names to ignore in log.""" + return self.__blacklisted_names + + @property + def blacklisted_exceptions(self) -> typing.List[typing.Type[Exception]]: + """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 ( + "{cls}(" + "log={self._logger}, " + "log_level={self.log_level}, " + "exc_level={self.exc_level}, " + "max_indent={self.max_indent}, " + "spec={spec}, " + "blacklisted_names={self.blacklisted_names}, " + "blacklisted_exceptions={self.blacklisted_exceptions}, " + "log_call_args={self.log_call_args}, " + "log_call_args_on_exc={self.log_call_args_on_exc}, " + "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.""" + return arg + + def post_process_param(self, arg: BoundParameter, str arg_repr: str) -> str: + """Process parameter for the future logging.""" + return arg_repr + + 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 "" + + param_str = "" + + 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.""" + 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 + + ) + ) + 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 + 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.""" + sig = inspect.signature(self._spec or func) + + @functools.wraps(func) + @asyncio.coroutine + def async_wrapper(*args, **kwargs): # type: (typing.Any, typing.Any) -> typing.Any + args_repr = self._get_func_args_repr(sig=sig, args=args, kwargs=kwargs) + + try: + self._make_calling_record(name=func.__name__, arguments=args_repr, method="Awaiting") + result = yield from func(*args, **kwargs) + self._make_done_record(func.__name__, result) + except BaseException as e: + if isinstance(e, tuple(self.blacklisted_exceptions)): + raise + self._make_exc_record(name=func.__name__, arguments=args_repr) + raise + return result + + @functools.wraps(func) + def wrapper(*args, **kwargs): # type: (typing.Any, typing.Any) -> typing.Any + args_repr = self._get_func_args_repr(sig=sig, args=args, kwargs=kwargs) + + try: + self._make_calling_record(name=func.__name__, arguments=args_repr) + result = func(*args, **kwargs) + self._make_done_record(func.__name__, result) + except BaseException as e: + if isinstance(e, tuple(self.blacklisted_exceptions)): + raise + self._make_exc_record(name=func.__name__, arguments=args_repr) + raise + return result + + return async_wrapper if asyncio.iscoroutinefunction(func) else wrapper + + def __call__( + self, *args: typing.Union[typing.Callable, typing.Any], **kwargs: typing.Any + ) -> typing.Union[typing.Callable[..., typing.Any], typing.Any]: + """Callable instance.""" + return super(LogWrap, self).__call__(*args, **kwargs) + + +def logwrap( + func: typing.Optional[typing.Callable] = None, + *, + log: logging.Logger = logger, + unsigned int log_level=logging.DEBUG, + unsigned int exc_level=logging.ERROR, + unsigned int max_indent=20, + spec: typing.Optional[typing.Callable] = None, + blacklisted_names: typing.Optional[typing.Iterable[str]] = None, + blacklisted_exceptions: typing.Optional[typing.Iterable[typing.Type[Exception]]] = None, + bint log_call_args=True, + bint log_call_args_on_exc=True, + bint log_traceback=True, + bint log_result_obj=True +) -> typing.Union[LogWrap, typing.Callable]: + """Log function calls and return values.""" + wrapper = LogWrap( + log=log, + log_level=log_level, + exc_level=exc_level, + max_indent=max_indent, + spec=spec, + blacklisted_names=blacklisted_names, + blacklisted_exceptions=blacklisted_exceptions, + log_call_args=log_call_args, + log_call_args_on_exc=log_call_args_on_exc, + log_traceback=log_traceback, + log_result_obj=log_result_obj, + ) + if func is not None: + return wrapper(func) + return wrapper diff --git a/logwrap/repr_utils.pxd b/logwrap/repr_utils.pxd new file mode 100644 index 0000000..fefd06d --- /dev/null +++ b/logwrap/repr_utils.pxd @@ -0,0 +1,39 @@ +# Copyright 2018 Alexey Stepanov aka penguinolog + +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""repr_utils module. + +This is no reason to import this submodule directly, all required methods is +available from the main module. +""" + +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]) diff --git a/logwrap/_repr_utils.py b/logwrap/repr_utils.py similarity index 99% rename from logwrap/_repr_utils.py rename to logwrap/repr_utils.py index 47ceebf..870401a 100644 --- a/logwrap/_repr_utils.py +++ b/logwrap/repr_utils.py @@ -139,7 +139,7 @@ def _prepare_repr(func: typing.Union[types.FunctionType, types.MethodType]) -> t # pylint: enable=no-member -class PrettyFormat: +class PrettyFormat(metaclass=abc.ABCMeta): """Pretty Formatter. Designed for usage as __repr__ and __str__ replacement on complex objects diff --git a/logwrap/repr_utils.pyx b/logwrap/repr_utils.pyx new file mode 100644 index 0000000..3a10a53 --- /dev/null +++ b/logwrap/repr_utils.pyx @@ -0,0 +1,386 @@ +# Copyright 2018 Alexey Stepanov aka penguinolog + +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""repr_utils module. + +This is no reason to import this submodule directly, all required methods is +available from the main module. +""" + +import inspect +import types +import typing + + +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)) + + +class ReprParameter: + """Parameter wrapper wor repr and str operations over signature.""" + + __slots__ = ("_value", "_parameter") + + POSITIONAL_ONLY = inspect.Parameter.POSITIONAL_ONLY + POSITIONAL_OR_KEYWORD = inspect.Parameter.POSITIONAL_OR_KEYWORD + VAR_POSITIONAL = inspect.Parameter.VAR_POSITIONAL + KEYWORD_ONLY = inspect.Parameter.KEYWORD_ONLY + VAR_KEYWORD = inspect.Parameter.VAR_KEYWORD + + empty = inspect.Parameter.empty + + def __init__(self, parameter: inspect.Parameter, value: typing.Optional[typing.Any] = None) -> None: + """Parameter-like object store for repr and str tasks. + + :param parameter: parameter from signature + :type parameter: inspect.Parameter + :param value: default value override + :type value: typing.Any + """ + self._parameter = parameter + self._value = value if value is not None else parameter.default + + @property + def parameter(self) -> inspect.Parameter: + """Parameter object.""" + return self._parameter + + @property + def name(self) -> typing.Union[None, str]: + """Parameter name. + + For `*args` and `**kwargs` add prefixes + """ + if self.kind == inspect.Parameter.VAR_POSITIONAL: + return "*" + self.parameter.name + if self.kind == inspect.Parameter.VAR_KEYWORD: + return "**" + self.parameter.name + return self.parameter.name + + @property + def value(self) -> typing.Any: + """Parameter value to log. + + If function is bound to class -> value is class instance else default value. + """ + return self._value + + @property + def annotation(self) -> typing.Union[inspect.Parameter.empty, str]: + """Parameter annotation.""" + return self.parameter.annotation + + @property + def kind(self) -> int: + """Parameter kind.""" + return self.parameter.kind # type: ignore + + # noinspection PyTypeChecker + def __hash__(self) -> int: + """Block hashing. + + :raises TypeError: Not hashable. + """ + msg = "unhashable type: '{0}'".format(self.__class__.__name__) + raise TypeError(msg) + + def __repr__(self) -> str: + """Debug purposes.""" + return '<{} "{}">'.format(self.__class__.__name__, self) + + +# 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) + if not ismethod: + real_func = func + else: + real_func = func.__func__ # type: ignore + + parameters = list(inspect.signature(real_func).parameters.values()) + + params = iter(parameters) + if ismethod and func.__self__ is not None: # type: ignore + try: + yield ReprParameter(next(params), value=func.__self__) # type: ignore + except StopIteration: + return + for arg in params: + yield ReprParameter(arg) + + +# pylint: enable=no-member + + +cdef class PrettyFormat: + """Pretty Formatter. + + Designed for usage as __repr__ and __str__ replacement on complex objects + """ + + def __cinit__(self, unsigned int max_indent=20, unsigned int indent_step=4): + """Pretty Formatter.""" + 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 + + cdef str _repr_callable(self, src: typing.Union[types.FunctionType, types.MethodType], unsigned int indent=0): + """Repr callable object (function or method).""" + raise NotImplementedError() + + cdef str _repr_simple(self, src: typing.Any, unsigned int indent=0, bint no_indent_start=False): + """Repr object without iteration.""" + raise NotImplementedError() + + def _repr_dict_items(self, dict src, unsigned int indent=0) -> typing.Iterator[str]: # type + """Repr dict items.""" + raise NotImplementedError() + + cdef str _repr_iterable_item(self, bint nl, str obj_type, str prefix, unsigned int indent, str result, str suffix): + """Repr iterable item.""" + raise NotImplementedError() + + def _repr_iterable_items(self, src: typing.Iterable, unsigned int indent=0) -> typing.Iterator[str]: + """Repr iterable items (not designed for dicts).""" + 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.""" + if hasattr(src, self._magic_method_name): + result = getattr(src, self._magic_method_name)(self, indent=indent, no_indent_start=no_indent_start) + return result # type: ignore + + if _known_callable(src): + return self._repr_callable(src=src, indent=indent) + + if _simple(src) or indent >= self.max_indent or not src: + return self._repr_simple(src=src, indent=indent, no_indent_start=no_indent_start) + + if isinstance(src, dict): + prefix, suffix = "{", "}" + result = "".join(self._repr_dict_items(src=src, indent=indent)) + else: + if isinstance(src, list): + prefix, suffix = "[", "]" + elif isinstance(src, tuple): + prefix, suffix = "(", ")" + else: + prefix, suffix = "{", "}" + result = "".join(self._repr_iterable_items(src=src, indent=indent)) + return self._repr_iterable_item( + nl=no_indent_start, + obj_type=src.__class__.__name__, + prefix=prefix, + indent=indent, + result=result, + suffix=suffix, + ) + + 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.""" + result = self.process_element(src, indent=indent, no_indent_start=no_indent_start) + return result + + +cdef class PrettyRepr(PrettyFormat): + """Pretty repr. + + Designed for usage as __repr__ replacement on complex objects + """ + + __slots__ = () + + 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.""" + 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) + + def _repr_dict_items(self, dict src, unsigned int indent=0) -> typing.Iterator[str]: + """Repr dict items.""" + max_len = max((len(repr(key)) for key in src)) if src else 0 + for key, val in src.items(): + yield "\n{spc:<{indent}}{key!r:{size}}: {val},".format( + spc="", + indent=self.next_indent(indent), + size=max_len, + key=key, + 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).""" + param_str = "" + + 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.""" + + 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) + + def _repr_dict_items(self, dict src, unsigned int indent=0) -> typing.Iterator[str]: + """Repr dict items.""" + 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( + spc="", + indent=self.next_indent(indent), + size=max_len, + key=key, + 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).""" + param_str = "" + + 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( + 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.""" + return PrettyRepr(max_indent=max_indent, indent_step=indent_step)( + src=src, indent=indent, no_indent_start=no_indent_start + ) + + +def 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.""" + return PrettyStr(max_indent=max_indent, indent_step=indent_step)( + src=src, indent=indent, no_indent_start=no_indent_start + ) diff --git a/setup.py b/setup.py index 3088001..1c7d434 100644 --- a/setup.py +++ b/setup.py @@ -50,9 +50,9 @@ def _extension(modpath): requires_optimization = [ - _extension("logwrap._class_decorator"), - _extension("logwrap._log_wrap"), - _extension("logwrap._repr_utils"), + setuptools.Extension("logwrap.class_decorator", ["logwrap/class_decorator.pyx"]), + setuptools.Extension("logwrap.log_wrap", ["logwrap/log_wrap.pyx"]), + setuptools.Extension("logwrap.repr_utils", ["logwrap/repr_utils.pyx"]), ] if "win32" != sys.platform: @@ -92,9 +92,6 @@ def run(self): src_files = ( os.path.join("logwrap", "__init__.py"), - # _log_wrap3 should not be compiled due to specific bug: - # Exception inside `async def` crashes python. - os.path.join("logwrap", "_log_wrap.py"), ) for src_file in src_files: diff --git a/test/test_log_wrap_shared.py b/test/test_log_wrap_shared.py index 98a3833..f326055 100644 --- a/test/test_log_wrap_shared.py +++ b/test/test_log_wrap_shared.py @@ -20,7 +20,7 @@ import unittest # noinspection PyProtectedMember -from logwrap import _log_wrap +from logwrap import log_wrap def example_function( @@ -35,7 +35,7 @@ def example_function( # noinspection PyUnusedLocal,PyMissingOrEmptyDocstring class TestBind(unittest.TestCase): def test_001_positive(self): - params = list(_log_wrap.bind_args_kwargs(sig, 1, arg3=33)) + params = list(log_wrap.bind_args_kwargs(sig, 1, arg3=33)) arg_1_bound = params[0] self.assertEqual(arg_1_bound.name, 'arg1') self.assertEqual(arg_1_bound.value, 1) @@ -77,7 +77,7 @@ def test_001_positive(self): self.assertEqual(str(kwargs_bound), "**kwargs={}") def test_002_args_kwargs(self): - params = list(_log_wrap.bind_args_kwargs(sig, 1, 2, 3, 4, arg5=5)) + params = list(log_wrap.bind_args_kwargs(sig, 1, 2, 3, 4, arg5=5)) args_bound = params[3] self.assertEqual(args_bound.name, 'args') @@ -96,18 +96,18 @@ def test_002_args_kwargs(self): self.assertEqual(str(kwargs_bound), "**kwargs={'arg5': 5}") def test_003_no_value(self): - params = list(_log_wrap.bind_args_kwargs(sig, 1, arg3=33)) + params = list(log_wrap.bind_args_kwargs(sig, 1, arg3=33)) arg_1_bound = params[0] arg1_parameter = arg_1_bound.parameter with self.assertRaises(ValueError): - _log_wrap.BoundParameter(arg1_parameter, arg1_parameter.empty) + log_wrap.BoundParameter(arg1_parameter, arg1_parameter.empty) def test_004_annotations(self): - namespace = {} - exec("""def func(arg1, arg2: int, arg3: int=3): pass""", namespace) - func = namespace['func'] + def func(arg1, arg2: int, arg3: int = 3): + pass + sig = signature(func) - params = list(_log_wrap.bind_args_kwargs(sig, 1, 2, 4)) + params = list(log_wrap.bind_args_kwargs(sig, 1, 2, 4)) arg_1_bound = params[0] self.assertEqual(arg_1_bound.name, 'arg1') diff --git a/test/test_repr_utils.py b/test/test_repr_utils.py index 46f64a4..b7da7fe 100644 --- a/test/test_repr_utils.py +++ b/test/test_repr_utils.py @@ -18,6 +18,7 @@ """_repr_utils (internal helpers) specific tests.""" +import typing import unittest import logwrap @@ -285,16 +286,9 @@ def __pretty_repr__( class TestAnnotated(unittest.TestCase): def test_001_annotation_args(self): fmt = "\n{spc:<{indent}}<{obj!r} with interface ({args}){annotation}>".format - namespace = {} - exec(""" -import typing -def func(a: typing.Optional[int]=None): - pass - """, - namespace - ) - func = namespace['func'] # type: typing.Callable[..., None] + def func(a: typing.Optional[int] = None): + pass self.assertEqual( logwrap.pretty_repr(func), @@ -320,16 +314,9 @@ def func(a: typing.Optional[int]=None): def test_002_annotation_return(self): fmt = "\n{spc:<{indent}}<{obj!r} with interface ({args}){annotation}>".format - namespace = {} - exec(""" -import typing -def func() -> None: - pass - """, - namespace - ) - func = namespace['func'] # type: typing.Callable[[], None] + def func() -> None: + pass self.assertEqual( logwrap.pretty_repr(func), @@ -355,16 +342,9 @@ def func() -> None: def test_003_complex(self): fmt = "\n{spc:<{indent}}<{obj!r} with interface ({args}){annotation}>".format - namespace = {} - exec(""" -import typing -def func(a: typing.Optional[int]=None) -> None: - pass - """, - namespace - ) - func = namespace['func'] # type: typing.Callable[..., None] + def func(a: typing.Optional[int] = None) -> None: + pass self.assertEqual( logwrap.pretty_repr(func), From 4f81de243470703de00dc2a0f617b89e3f6e416c Mon Sep 17 00:00:00 2001 From: Alexey Stepanov Date: Thu, 8 Nov 2018 12:56:39 +0100 Subject: [PATCH 2/2] Help cython with types (#52) (cherry picked from commit fb65688bab868258ae212d95ba36c71e3f6f1e6c) Signed-off-by: Alexey Stepanov --- logwrap/__init__.pxd | 1 + logwrap/__init__.pyx | 17 +++++++++++++++++ logwrap/class_decorator.pyx | 2 +- logwrap/log_wrap.pxd | 2 +- logwrap/log_wrap.pyx | 16 ++++++++++------ logwrap/repr_utils.pyx | 20 ++++++++++++++++---- 6 files changed, 46 insertions(+), 12 deletions(-) create mode 100644 logwrap/__init__.pxd diff --git a/logwrap/__init__.pxd b/logwrap/__init__.pxd new file mode 100644 index 0000000..802ae15 --- /dev/null +++ b/logwrap/__init__.pxd @@ -0,0 +1 @@ +cdef tuple __all__ diff --git a/logwrap/__init__.pyx b/logwrap/__init__.pyx index e69de29..e57c6c6 100644 --- a/logwrap/__init__.pyx +++ b/logwrap/__init__.pyx @@ -0,0 +1,17 @@ +from .repr_utils cimport PrettyFormat, PrettyRepr, PrettyStr +from .repr_utils import pretty_repr, pretty_str + +from .log_wrap cimport LogWrap +from .log_wrap import logwrap, BoundParameter, bind_args_kwargs + +cdef tuple __all__ = ( + "LogWrap", + "logwrap", + "PrettyFormat", + "PrettyRepr", + "PrettyStr", + "pretty_repr", + "pretty_str", + "BoundParameter", + "bind_args_kwargs", +) diff --git a/logwrap/class_decorator.pyx b/logwrap/class_decorator.pyx index 6d0b851..ca0c81e 100644 --- a/logwrap/class_decorator.pyx +++ b/logwrap/class_decorator.pyx @@ -37,7 +37,7 @@ cdef class BaseDecorator: def __call__(self, *args: typing.Union[typing.Callable, typing.Any], **kwargs: typing.Any) -> typing.Any: """Main decorator getter.""" - l_args = list(args) + cdef list l_args = list(args) if self._func: wrapped = self._func # type: typing.Callable diff --git a/logwrap/log_wrap.pxd b/logwrap/log_wrap.pxd index c624c5a..5308c21 100644 --- a/logwrap/log_wrap.pxd +++ b/logwrap/log_wrap.pxd @@ -17,7 +17,7 @@ import inspect import typing -from . cimport class_decorator +from logwrap cimport class_decorator cdef unsigned int indent diff --git a/logwrap/log_wrap.pyx b/logwrap/log_wrap.pyx index 4dc0a14..eda0d9f 100644 --- a/logwrap/log_wrap.pyx +++ b/logwrap/log_wrap.pyx @@ -22,11 +22,11 @@ import sys import traceback import typing -from . cimport repr_utils -from . cimport class_decorator +from logwrap cimport repr_utils +from logwrap cimport class_decorator -__all__ = ("LogWrap", "logwrap", "BoundParameter", "bind_args_kwargs") +cdef tuple __all__ = ("LogWrap", "logwrap", "BoundParameter", "bind_args_kwargs") logger = logging.getLogger("logwrap") # type: logging.Logger @@ -111,6 +111,8 @@ class BoundParameter: def __str__(self) -> str: """Debug purposes.""" + cdef str as_str + # POSITIONAL_ONLY is only in precompiled functions if self.kind == self.POSITIONAL_ONLY: as_str = "" if self.name is None else "<{as_str}>".format(as_str=self.name) @@ -253,7 +255,9 @@ cdef class LogWrap(class_decorator.BaseDecorator): if not (self.log_call_args or self.log_call_args_on_exc): return "" - param_str = "" + cdef str param_str = "" + cdef str val + cdef str annotation last_kind = None for param in bind_args_kwargs(sig, *args, **kwargs): @@ -295,7 +299,7 @@ cdef class LogWrap(class_decorator.BaseDecorator): cdef void _make_done_record(self, str func_name, result: typing.Any): """Construct success record.""" - msg = "Done: {name!r}".format(name=func_name) + cdef str msg = "Done: {name!r}".format(name=func_name) if self.log_result_obj: msg += " with result:\n{result}".format( @@ -327,7 +331,7 @@ cdef class LogWrap(class_decorator.BaseDecorator): full_tb = stack[:2] + tb # cut decorator and build full traceback exc_line = traceback.format_exception_only(*exc_info[:2]) # Make standard traceback string - tb_text = "Traceback (most recent call last):\n" + "".join(traceback.format_list(full_tb)) + "".join(exc_line) + 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, diff --git a/logwrap/repr_utils.pyx b/logwrap/repr_utils.pyx index 3a10a53..1f5c918 100644 --- a/logwrap/repr_utils.pyx +++ b/logwrap/repr_utils.pyx @@ -23,6 +23,9 @@ import types import typing +cdef 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)) @@ -168,6 +171,10 @@ cdef class PrettyFormat: 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 + if hasattr(src, self._magic_method_name): result = getattr(src, self._magic_method_name)(self, indent=indent, no_indent_start=no_indent_start) return result # type: ignore @@ -217,6 +224,8 @@ cdef class PrettyRepr(PrettyFormat): 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" @@ -235,7 +244,8 @@ cdef class PrettyRepr(PrettyFormat): def _repr_dict_items(self, dict src, unsigned int indent=0) -> typing.Iterator[str]: """Repr dict items.""" - max_len = max((len(repr(key)) for key in src)) if src else 0 + cdef unsigned int max_len = max((len(repr(key)) for key in src)) if src else 0 + for key, val in src.items(): yield "\n{spc:<{indent}}{key!r:{size}}: {val},".format( spc="", @@ -247,7 +257,8 @@ cdef class PrettyRepr(PrettyFormat): cdef str _repr_callable(self, src: typing.Union[types.FunctionType, types.MethodType], unsigned int indent=0): """Repr callable object (function or method).""" - param_str = "" + 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) @@ -312,7 +323,7 @@ cdef class PrettyStr(PrettyFormat): def _repr_dict_items(self, dict src, unsigned int indent=0) -> typing.Iterator[str]: """Repr dict items.""" - max_len = max((len(str(key)) for key in src)) if src else 0 + 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( spc="", @@ -324,7 +335,8 @@ cdef class PrettyStr(PrettyFormat): cdef str _repr_callable(self, src: typing.Union[types.FunctionType, types.MethodType], unsigned int indent=0): """Repr callable object (function or method).""" - param_str = "" + 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)