diff --git a/.travis.yml b/.travis.yml index 44ae2f6..94638e2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,13 +2,10 @@ sudo: false language: python os: linux python: -- 2.7 -- 3.4 - 3.5 - 3.6 - &mainstream_python 3.7-dev -- &pypy pypy -- pypy3.5 +- &pypy pypy3.5 install: - &upgrade_python_toolset pip install --upgrade pip setuptools wheel - pip install tox-travis diff --git a/CI_REQUIREMENTS.txt b/CI_REQUIREMENTS.txt index 6476c67..bc04b49 100644 --- a/CI_REQUIREMENTS.txt +++ b/CI_REQUIREMENTS.txt @@ -1,3 +1 @@ -funcsigs >= 1.0 -mock; python_version == "2.7" -r requirements.txt diff --git a/README.rst b/README.rst index 03d3821..877462d 100644 --- a/README.rst +++ b/README.rst @@ -39,14 +39,13 @@ Pros: :: - Python 2.7 - Python 3.4 Python 3.5 Python 3.6 Python 3.7 - PyPy PyPy3 3.5+ +.. note:: Python 3.4 and 2.7 is supported in versions < 5.0.0 + This package includes helpers: * `logwrap` - main helper. The same is `LogWrap`. @@ -207,7 +206,6 @@ Signature is self-documenting: no_indent_start=False, # do not indent the first level max_indent=20, # maximum allowed indent level indent_step=4, # step between indents - py2_str=False, # use bytes for python 2 __repr__ and __str__ ) Limitation: Dict like objects is always marked inside `{}` for readability, even if it is `collections.OrderedDict` (standard repr as list of tuples). @@ -225,7 +223,6 @@ Signature is self-documenting: no_indent_start=False, # do not indent the first level max_indent=20, # maximum allowed indent level indent_step=4, # step between indents - py2_str=False, # use bytes for python 2 __repr__ and __str__ ) Limitations: @@ -248,7 +245,6 @@ Object signature: self, max_indent=20, # maximum allowed indent level indent_step=4, # step between indents - py2_str=False, # use bytes for python 2 __repr__ and __str__ ) Callable object (`PrettyFormat` instance) signature: diff --git a/appveyor.yml b/appveyor.yml index 22ab6b3..0a57f36 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -4,18 +4,6 @@ environment: secure: TCKGf77kkVeo2Pbd+lQY5Q== matrix: - - PYTHON: "C:\\Python27-x64" - PYTHON_VERSION: "2.7.x" # currently 2.7.11 - PYTHON_ARCH: "64" - - - PYTHON: "C:\\Python34" - PYTHON_VERSION: "3.4.x" # currently 3.4.3 - PYTHON_ARCH: "32" - - - PYTHON: "C:\\Python34-x64" - PYTHON_VERSION: "3.4.x" # currently 3.4.3 - PYTHON_ARCH: "64" - - PYTHON: "C:\\Python35" PYTHON_VERSION: "3.5.x" # currently 3.5.1 PYTHON_ARCH: "32" @@ -54,7 +42,7 @@ install: - pip install pytest pytest-sugar - pip install -r build_requirements.txt -build: false +build: off cache: - '%LOCALAPPDATA%\pip\Cache' diff --git a/doc/source/PrettyFormat.rst b/doc/source/PrettyFormat.rst index 8dc7adf..8a8e55a 100644 --- a/doc/source/PrettyFormat.rst +++ b/doc/source/PrettyFormat.rst @@ -6,14 +6,12 @@ API: Helpers: `pretty_repr`, `pretty_str` and base class `PrettyFormat`. .. py:module:: logwrap .. py:currentmodule:: logwrap -.. py:function:: pretty_repr(src, indent=0, no_indent_start=False, max_indent=20, indent_step=4, py2_str=False, ) +.. py:function:: pretty_repr(src, indent=0, no_indent_start=False, max_indent=20, indent_step=4, ) Make human readable repr of object. :param src: object to process - :type src: typing.Union[ - typing.AnyStr, int, typing.Iterable, object - ] + :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 @@ -22,22 +20,18 @@ API: Helpers: `pretty_repr`, `pretty_str` and base class `PrettyFormat`. :type max_indent: int :param indent_step: step for the next indentation level :type indent_step: int - :param py2_str: use Python 2.x compatible strings instead of unicode - :type py2_str: bool :return: formatted string :rtype: str -.. py:function:: pretty_str(src, indent=0, no_indent_start=False, max_indent=20, indent_step=4, py2_str=False, ) +.. py:function:: pretty_str(src, indent=0, no_indent_start=False, max_indent=20, indent_step=4, ) Make human readable str of object. .. versionadded:: 1.1.0 :param src: object to process - :type src: typing.Union[ - typing.AnyStr, int, typing.Iterable, object - ] + :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 @@ -46,8 +40,6 @@ API: Helpers: `pretty_repr`, `pretty_str` and base class `PrettyFormat`. :type max_indent: int :param indent_step: step for the next indentation level :type indent_step: int - :param py2_str: use Python 2.x compatible strings instead of unicode - :type py2_str: bool :return: formatted string :rtype: str @@ -59,14 +51,12 @@ API: Helpers: `pretty_repr`, `pretty_str` and base class `PrettyFormat`. .. versionadded:: 1.0.2 .. versionchanged:: 3.0.1 - .. py:method:: __init__(max_indent=20, indent_step=4, py2_str=False, ) + .. py:method:: __init__(max_indent=20, indent_step=4, ) :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 - :param py2_str: use Python 2.x compatible strings instead of unicode - :type py2_str: bool .. note:: Attributes is read-only @@ -89,9 +79,7 @@ API: Helpers: `pretty_repr`, `pretty_str` and base class `PrettyFormat`. Make human readable representation of object. :param src: object to process - :type src: typing.Union[ - typing.AnyStr, int, typing.Iterable, object - ] + :type src: typing.Any :param indent: start indentation :type indent: int :param no_indent_start: @@ -105,9 +93,7 @@ API: Helpers: `pretty_repr`, `pretty_str` and base class `PrettyFormat`. Make human readable representation of object. The main entry point. :param src: object to process - :type src: typing.Union[ - typing.AnyStr, int, typing.Iterable, object - ] + :type src: typing.Any :param indent: start indentation :type indent: int :param no_indent_start: @@ -124,14 +110,12 @@ API: Helpers: `pretty_repr`, `pretty_str` and base class `PrettyFormat`. .. versionadded:: 3.0.0 .. versionchanged:: 3.0.1 - .. py:method:: __init__(max_indent=20, indent_step=4, py2_str=False, ) + .. py:method:: __init__(max_indent=20, indent_step=4, ) :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 - :param py2_str: use Python 2.x compatible strings instead of unicode - :type py2_str: bool .. py:class:: PrettyStr(PrettyFormat) @@ -141,11 +125,9 @@ API: Helpers: `pretty_repr`, `pretty_str` and base class `PrettyFormat`. .. versionadded:: 3.0.0 .. versionchanged:: 3.0.1 - .. py:method:: __init__(max_indent=20, indent_step=4, py2_str=False, ) + .. py:method:: __init__(max_indent=20, indent_step=4, ) :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 - :param py2_str: use Python 2.x compatible strings instead of unicode - :type py2_str: bool diff --git a/logwrap/__init__.py b/logwrap/__init__.py index 607152e..c816be1 100644 --- a/logwrap/__init__.py +++ b/logwrap/__init__.py @@ -22,10 +22,6 @@ later it has been reworked and extended for support of special cases. """ -from __future__ import absolute_import - -import sys - from ._repr_utils import ( PrettyFormat, PrettyRepr, @@ -33,16 +29,7 @@ pretty_repr, pretty_str ) -from ._log_wrap_shared import BoundParameter, bind_args_kwargs - -PY3 = sys.version_info[:2] > (3, 0) # type: bool - -# pylint: disable=no-name-in-module -if PY3: # pragma: no cover - from ._log_wrap3 import logwrap, LogWrap -else: # pragma: no cover - from ._log_wrap2 import logwrap, LogWrap -# pylint: enable=no-name-in-module +from ._log_wrap import logwrap, LogWrap, BoundParameter, bind_args_kwargs __all__ = ( 'LogWrap', @@ -56,7 +43,7 @@ 'bind_args_kwargs' ) -__version__ = '4.0.1' +__version__ = '5.0.0' __author__ = "Alexey Stepanov" __author_email__ = 'penguinolog@gmail.com' __maintainers__ = { diff --git a/logwrap/_class_decorator.py b/logwrap/_class_decorator.py index ac18778..c961626 100644 --- a/logwrap/_class_decorator.py +++ b/logwrap/_class_decorator.py @@ -16,17 +16,12 @@ """Base class for decorators.""" -from __future__ import absolute_import -from __future__ import print_function - import abc import functools -import typing # noqa # pylint: disable=unused-import - -import six +import typing -class BaseDecorator(six.with_metaclass(abc.ABCMeta, object)): +class BaseDecorator(metaclass=abc.ABCMeta): """Base class for decorators. Implements wrapping and __call__, wrapper getter is abstract. @@ -69,27 +64,25 @@ class BaseDecorator(six.with_metaclass(abc.ABCMeta, object)): def __init__( self, - func=None # type: typing.Optional[typing.Callable] - ): # type: (...) -> None + func: typing.Optional[typing.Callable] = None + ) -> None: """Decorator. :param func: function to wrap :type func: typing.Optional[typing.Callable] """ + # 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) - if not six.PY3: # pragma: no cover - self.__wrapped__ = self.__func # type: typing.Callable # pylint: enable=assigning-non-slot - # noinspection PyArgumentList - super(BaseDecorator, self).__init__() @property def _func( self - ): # type: () -> typing.Optional[typing.Callable] + ) -> typing.Optional[typing.Callable]: """Get wrapped function. :rtype: typing.Optional[typing.Callable] @@ -99,8 +92,8 @@ def _func( @abc.abstractmethod def _get_function_wrapper( self, - func # type: typing.Callable - ): # type: (...) -> typing.Callable + func: typing.Callable + ) -> typing.Callable: """Here should be constructed and returned real decorator. :param func: Wrapped function @@ -111,18 +104,23 @@ def _get_function_wrapper( def __call__( self, - *args, # type: typing.Any - **kwargs # type: typing.Any - ): # type: (...) -> typing.Any + *args: typing.Union[typing.Tuple, typing.Callable], + **kwargs: typing.Dict + ) -> typing.Any: """Main decorator getter.""" - args = list(args) - wrapped = self.__func or args.pop(0) + l_args = list(args) + + if self._func: + wrapped = self._func # type: typing.Callable + else: + wrapped = l_args.pop(0) # type: ignore + wrapper = self._get_function_wrapper(wrapped) if self.__func: - return wrapper(*args, **kwargs) + return wrapper(*l_args, **kwargs) return wrapper - def __repr__(self): + def __repr__(self) -> str: """For debug purposes.""" return "<{cls}({func!r}) at 0x{id:X}>".format( cls=self.__class__.__name__, diff --git a/logwrap/_class_decorator.pyi b/logwrap/_class_decorator.pyi deleted file mode 100644 index ba35c5b..0000000 --- a/logwrap/_class_decorator.pyi +++ /dev/null @@ -1,14 +0,0 @@ -import abc -import typing - - -class BaseDecorator(object, metaclass=abc.ABCMeta): - def __init__(self, func: typing.Optional[typing.Callable] = ...) -> None: ... - - @property - def _func(self) -> typing.Optional[typing.Callable]: ... - - @abc.abstractmethod - def _get_function_wrapper(self, func: typing.Callable) -> typing.Callable: ... - - def __call__(self, *args: typing.Tuple, **kwargs: typing.Dict) -> typing.Any: ... diff --git a/logwrap/_log_wrap_shared.py b/logwrap/_log_wrap.py similarity index 54% rename from logwrap/_log_wrap_shared.py rename to logwrap/_log_wrap.py index 8bbc42a..505dde4 100644 --- a/logwrap/_log_wrap_shared.py +++ b/logwrap/_log_wrap.py @@ -16,34 +16,22 @@ """log_wrap shared code module.""" -from __future__ import absolute_import -from __future__ import unicode_literals - +import asyncio import functools -import inspect # noqa # pylint: disable=unused-import +import inspect import logging -import typing # noqa # pylint: disable=unused-import - -import six # noqa # pylint: disable=unused-import +import typing import logwrap as core from . import _class_decorator -# pylint: disable=ungrouped-imports, no-name-in-module -if six.PY3: # pragma: no cover - from inspect import formatannotation - from inspect import Parameter - from inspect import Signature # noqa # pylint: disable=unused-import -else: # pragma: no cover - # noinspection PyUnresolvedReferences,PyProtectedMember,PyPackageRequirements - from funcsigs import formatannotation - # noinspection PyUnresolvedReferences,PyPackageRequirements - from funcsigs import Parameter - # noinspection PyUnresolvedReferences,PyPackageRequirements - from funcsigs import Signature # noqa # pylint: disable=unused-import -# pylint: enable=ungrouped-imports, no-name-in-module - -__all__ = ('BaseLogWrap', 'BoundParameter', 'bind_args_kwargs') + +__all__ = ( + 'LogWrap', + 'logwrap', + 'BoundParameter', + 'bind_args_kwargs', +) logger = logging.getLogger('logwrap') # type: logging.Logger @@ -53,32 +41,7 @@ comment = "\n{spc:<{indent}}# {{kind!s}}:".format(spc='', indent=indent).format -def _check_type(expected): # type: (typing.Type) -> typing.Callable - """Check type before assign. - - :type expected: type - """ - def deco(func): # type: (typing.Callable) -> typing.Callable - """Check type before assign.""" - # pylint: disable=missing-docstring - # noinspection PyMissingOrEmptyDocstring - @functools.wraps(func) - def wrapper(self, val): - if not isinstance(val, expected): - raise TypeError( - 'Unexpected type: {}. Should be {}.'.format( - val.__class__.__name__, - expected.__name__, - ) - ) - return func(self, val) - - # pylint: enable=missing-docstring - return wrapper - return deco - - -class BoundParameter(object): +class BoundParameter: """Parameter-like object store BOUND with value parameter. .. versionadded:: 3.3.0 @@ -89,19 +52,19 @@ class BoundParameter(object): '_value' ) - POSITIONAL_ONLY = Parameter.POSITIONAL_ONLY - POSITIONAL_OR_KEYWORD = Parameter.POSITIONAL_OR_KEYWORD - VAR_POSITIONAL = Parameter.VAR_POSITIONAL - KEYWORD_ONLY = Parameter.KEYWORD_ONLY - VAR_KEYWORD = Parameter.VAR_KEYWORD + 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 = Parameter.empty + empty = inspect.Parameter.empty def __init__( self, - parameter, # type: Parameter - value=Parameter.empty # type: typing.Any - ): # type: (...) -> None + parameter: inspect.Parameter, + value: typing.Any = inspect.Parameter.empty + ) -> None: """Parameter-like object store BOUND with value parameter. :param parameter: parameter from signature @@ -120,36 +83,36 @@ def __init__( self._value = value @property - def parameter(self): # type: () -> Parameter + def parameter(self) -> inspect.Parameter: """Parameter object.""" return self._parameter @property - def name(self): # type: () -> typing.Union[None, str] + def name(self) -> typing.Union[None, str]: """Parameter name.""" return self.parameter.name @property - def default(self): # type: () -> typing.Any + def default(self) -> typing.Any: """Parameter default value.""" return self.parameter.default @property - def annotation(self): # type: () -> typing.Union[Parameter.empty, str] + def annotation(self) -> typing.Union[inspect.Parameter.empty, str]: """Parameter annotation.""" return self.parameter.annotation @property - def kind(self): # type: () -> int + def kind(self) -> int: """Parameter kind.""" - return self.parameter.kind + return self.parameter.kind # type: ignore @property - def value(self): # type: () -> typing.Any + def value(self) -> typing.Any: """Parameter value.""" return self._value - def __hash__(self): # pragma: no cover + def __hash__(self) -> int: # pragma: no cover """Block hashing. :raises TypeError: Not hashable. @@ -157,17 +120,17 @@ def __hash__(self): # pragma: no cover msg = "unhashable type: '{0}'".format(self.__class__.__name__) raise TypeError(msg) - def __str__(self): + def __str__(self) -> str: """Debug purposes.""" - as_str = self.name - # POSITIONAL_ONLY is only in precompiled functions if self.kind == self.POSITIONAL_ONLY: # pragma: no cover - as_str = '' if as_str is None else '<{as_str}>'.format(as_str=as_str) + 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: # pragma: no cover - as_str += ': {annotation!s}'.format(annotation=formatannotation(self.annotation)) + as_str += ': {annotation!s}'.format(annotation=inspect.formatannotation(self.annotation)) value = self.value if self.empty == value: @@ -188,20 +151,20 @@ def __str__(self): return as_str - def __repr__(self): + def __repr__(self) -> str: """Debug purposes.""" return '<{} "{}">'.format(self.__class__.__name__, self) def bind_args_kwargs( - sig, # type: Signature - *args, - **kwargs -): # type: (...) -> typing.Iterator[BoundParameter] + sig: inspect.Signature, + *args: typing.Tuple, + **kwargs: typing.Dict +) -> typing.Iterator[BoundParameter]: """Bind *args and **kwargs to signature and get Bound Parameters. :param sig: source signature - :type sig: Signature + :type sig: inspect.Signature :return: Iterator for bound parameters with all information about it :rtype: typing.Iterator[BoundParameter] @@ -218,7 +181,7 @@ def bind_args_kwargs( # pylint: disable=assigning-non-slot,abstract-method # noinspection PyAbstractClass -class BaseLogWrap(_class_decorator.BaseDecorator): +class LogWrap(_class_decorator.BaseDecorator): """Base class for LogWrap implementation.""" __slots__ = ( @@ -236,18 +199,19 @@ class BaseLogWrap(_class_decorator.BaseDecorator): def __init__( self, - func=None, # type: typing.Optional[typing.Callable] - log=logger, # type: logging.Logger - log_level=logging.DEBUG, # type: int - exc_level=logging.ERROR, # type: int - max_indent=20, # type: int - spec=None, # type: typing.Optional[typing.Callable] - blacklisted_names=None, # type: typing.Optional[typing.Iterable[str]] - blacklisted_exceptions=None, # type: typing.Optional[typing.Iterable[typing.Type[Exception]]] - log_call_args=True, # type: bool - log_call_args_on_exc=True, # type: bool - log_result_obj=True, # type: bool - ): # type: (...) -> None + func: typing.Optional[typing.Callable] = None, + *, + log: logging.Logger = logger, + log_level: int = logging.DEBUG, + exc_level: int = logging.ERROR, + max_indent: int = 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, + log_call_args: bool = True, + log_call_args_on_exc: bool = True, + log_result_obj: bool = True + ) -> None: """Log function calls and return values. :param func: function to wrap @@ -281,7 +245,7 @@ def __init__( .. versionchanged:: 3.3.0 Extract func from log and do not use Union. """ - super(BaseLogWrap, self).__init__(func=func) + super(LogWrap, self).__init__(func=func) # Typing fix: if blacklisted_names is None: @@ -289,7 +253,7 @@ def __init__( else: self.__blacklisted_names = list(blacklisted_names) if blacklisted_exceptions is None: - self.__blacklisted_exceptions = [] # type: typing.List[Exception] + self.__blacklisted_exceptions = [] # type: typing.List[typing.Type[Exception]] else: self.__blacklisted_exceptions = list(blacklisted_exceptions) @@ -306,7 +270,7 @@ def __init__( # We are not interested to pass any arguments to object @property - def log_level(self): # type: () -> int + def log_level(self) -> int: """Log level for normal behavior. :rtype: int @@ -314,16 +278,22 @@ def log_level(self): # type: () -> int return self.__log_level @log_level.setter - @_check_type(int) - def log_level(self, val): # type: (int) -> None + def log_level(self, val: int) -> None: """Log level for normal behavior. :type val: int """ + if not isinstance(val, int): + raise TypeError( + 'Unexpected type: {}. Should be {}.'.format( + val.__class__.__name__, + int.__name__, + ) + ) self.__log_level = val @property - def exc_level(self): # type: () -> int + def exc_level(self) -> int: """Log level for exceptions. :rtype: int @@ -331,16 +301,22 @@ def exc_level(self): # type: () -> int return self.__exc_level @exc_level.setter - @_check_type(int) - def exc_level(self, val): # type: (int) -> None + def exc_level(self, val: int) -> None: """Log level for exceptions. :type val: int """ + if not isinstance(val, int): + raise TypeError( + 'Unexpected type: {}. Should be {}.'.format( + val.__class__.__name__, + int.__name__, + ) + ) self.__exc_level = val @property - def max_indent(self): # type: () -> int + def max_indent(self) -> int: """Maximum indentation. :rtype: int @@ -348,16 +324,22 @@ def max_indent(self): # type: () -> int return self.__max_indent @max_indent.setter - @_check_type(int) - def max_indent(self, val): # type: (int) -> None + def max_indent(self, val: int) -> None: """Maximum indentation. :type val: int """ + if not isinstance(val, int): + raise TypeError( + 'Unexpected type: {}. Should be {}.'.format( + val.__class__.__name__, + int.__name__, + ) + ) self.__max_indent = val @property - def blacklisted_names(self): # type: () -> typing.List[str] + def blacklisted_names(self) -> typing.List[str]: """List of arguments names to ignore in log. :rtype: typing.List[str] @@ -365,9 +347,7 @@ def blacklisted_names(self): # type: () -> typing.List[str] return self.__blacklisted_names @property - def blacklisted_exceptions( - self - ): # type: () -> typing.List[typing.Type[Exception]] + def blacklisted_exceptions(self) -> typing.List[typing.Type[Exception]]: """List of exceptions to re-raise without log. :rtype: typing.List[typing.Type[Exception]] @@ -375,7 +355,7 @@ def blacklisted_exceptions( return self.__blacklisted_exceptions @property - def log_call_args(self): # type: () -> bool + def log_call_args(self) -> bool: """Flag: log call arguments before call. :rtype: bool @@ -383,16 +363,22 @@ def log_call_args(self): # type: () -> bool return self.__log_call_args @log_call_args.setter - @_check_type(bool) - def log_call_args(self, val): # type: (bool) -> None + def log_call_args(self, val: bool) -> None: """Flag: log call arguments before call. :type val: bool """ + if not isinstance(val, bool): + raise TypeError( + 'Unexpected type: {}. Should be {}.'.format( + val.__class__.__name__, + bool.__name__, + ) + ) self.__log_call_args = val @property - def log_call_args_on_exc(self): # type: () -> bool + def log_call_args_on_exc(self) -> bool: """Flag: log call arguments on exception. :rtype: bool @@ -400,16 +386,22 @@ def log_call_args_on_exc(self): # type: () -> bool return self.__log_call_args_on_exc @log_call_args_on_exc.setter - @_check_type(bool) - def log_call_args_on_exc(self, val): # type: (bool) -> None + def log_call_args_on_exc(self, val: bool) -> None: """Flag: log call arguments on exception. :type val: bool """ + if not isinstance(val, bool): + raise TypeError( + 'Unexpected type: {}. Should be {}.'.format( + val.__class__.__name__, + bool.__name__, + ) + ) self.__log_call_args_on_exc = val @property - def log_result_obj(self): # type: () -> bool + def log_result_obj(self) -> bool: """Flag: log result object. :rtype: bool @@ -417,16 +409,22 @@ def log_result_obj(self): # type: () -> bool return self.__log_result_obj @log_result_obj.setter - @_check_type(bool) - def log_result_obj(self, val): # type: (bool) -> None + def log_result_obj(self, val: bool) -> None: """Flag: log result object. :type val: bool """ + if not isinstance(val, bool): + raise TypeError( + 'Unexpected type: {}. Should be {}.'.format( + val.__class__.__name__, + bool.__name__, + ) + ) self.__log_result_obj = val @property - def _logger(self): # type: () -> logging.Logger + def _logger(self) -> logging.Logger: """logger instance. :rtype: logging.Logger @@ -434,14 +432,14 @@ def _logger(self): # type: () -> logging.Logger return self.__logger @property - def _spec(self): # type: () -> typing.Callable + def _spec(self) -> typing.Optional[typing.Callable]: """Spec for function arguments. :rtype: typing.Callable """ return self.__spec - def __repr__(self): + def __repr__(self) -> str: """Repr for debug purposes.""" return ( "{cls}(" @@ -463,26 +461,26 @@ def __repr__(self): @staticmethod def _bind_args_kwargs( - sig, # type: Signature - *args, - **kwargs - ): # type: (...) -> typing.Iterator[BoundParameter] + sig: inspect.Signature, + *args: typing.Tuple, + **kwargs: typing.Dict + ) -> typing.Iterator[BoundParameter]: """Bind *args and **kwargs to signature and get Bound Parameters. :param sig: source signature - :type sig: Signature + :type sig: inspect.Signature :return: Iterator for bound parameters with all information about it :rtype: typing.Iterator[BoundParameter] .. versionadded:: 3.3.0 """ - return bind_args_kwargs(sig, *args, **kwargs) + return bind_args_kwargs(sig, *args, **kwargs) # type: ignore # noinspection PyMethodMayBeStatic def pre_process_param( # pylint: disable=no-self-use self, - arg, # type: BoundParameter - ): # type: (...) -> typing.Union[BoundParameter, typing.Tuple[BoundParameter, typing.Any], None] + arg: BoundParameter + ) -> typing.Union[BoundParameter, typing.Tuple[BoundParameter, typing.Any], None]: """Process parameter for the future logging. :param arg: bound parameter @@ -497,19 +495,19 @@ def pre_process_param( # pylint: disable=no-self-use return arg # noinspection PyMethodMayBeStatic,PyUnusedLocal - def post_process_param( # pylint: disable=no-self-use,unused-argument + def post_process_param( # pylint: disable=unused-argument, no-self-use self, - arg, # type: BoundParameter - arg_repr # type: typing.Text - ): # type: (...) -> typing.Text + arg: BoundParameter, + arg_repr: str + ) -> str: """Process parameter for the future logging. :param arg: bound parameter :type arg: BoundParameter :param arg_repr: repr for value - :type arg_repr: typing.Text + :type arg_repr: str :return: processed repr for value - :rtype: typing.Text + :rtype: str Override this method if some modifications required for result of repr() over parameter @@ -519,16 +517,16 @@ def post_process_param( # pylint: disable=no-self-use,unused-argument def _get_func_args_repr( self, - sig, # type: inspect.Signature - args, # type: typing.Tuple - kwargs # type: typing.Dict[str, typing.Any] - ): # type: (...) -> typing.Text + sig: inspect.Signature, + args: typing.Tuple, + kwargs: typing.Dict[str, typing.Any] + ) -> str: """Internal helper for reducing complexity of decorator code. :type sig: inspect.Signature :type args: tuple :type kwargs: dict - :rtype: typing.Text + :rtype: str .. versionchanged:: 3.3.0 Use pre- and post- processing of params during execution """ @@ -586,9 +584,9 @@ def _get_func_args_repr( def _make_done_record( self, - func_name, # type: str - result # type: typing.Any - ): # type: (...) -> None + func_name: str, + result: typing.Any + ) -> None: """Construct success record. :type func_name: str @@ -603,24 +601,21 @@ def _make_done_record( max_indent=self.max_indent, ) ) - self._logger.log( - level=self.log_level, - msg=msg - ) + self._logger.log(level=self.log_level, msg=msg) # type: ignore def _make_calling_record( self, - name, # type: str - arguments, # type: str - method='Calling' # type: str - ): # type: (...) -> None + name: str, + arguments: str, + method: str = "Calling" + ) -> None: """Make log record before execution. :type name: str :type arguments: str :type method: str """ - self._logger.log( + self._logger.log( # type: ignore level=self.log_level, msg="{method}: \n{name!r}({arguments})".format( method=method, @@ -631,15 +626,15 @@ def _make_calling_record( def _make_exc_record( self, - name, # type: str - arguments # type: str - ): # type: (...) -> None + name: str, + arguments: str + ) -> None: """Make log record if exception raised. :type name: str :type arguments: str """ - self._logger.log( + self._logger.log( # type: ignore level=self.exc_level, msg="Failed: \n{name!r}({arguments})".format( name=name, @@ -648,5 +643,191 @@ def _make_exc_record( exc_info=True ) + def _get_function_wrapper( + self, + func: typing.Callable + ) -> typing.Callable: + """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) + + # pylint: disable=missing-docstring + # noinspection PyCompatibility,PyMissingOrEmptyDocstring + @functools.wraps(func) + async def async_wrapper( + *args, # type: typing.Tuple + **kwargs # type: typing.Dict + ) -> 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 = await 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 + + # noinspection PyCompatibility,PyMissingOrEmptyDocstring + @functools.wraps(func) + def wrapper( + *args, # type: typing.Tuple + **kwargs # type: typing.Dict + ) -> 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 + + # pylint: enable=missing-docstring + return async_wrapper if asyncio.iscoroutinefunction(func) else wrapper + + def __call__( # pylint: disable=useless-super-delegation + self, + *args: typing.Union[typing.Tuple, typing.Callable], + **kwargs: typing.Dict + ) -> typing.Union[typing.Callable[..., typing.Any], typing.Any]: + """Callable instance.""" + return super(LogWrap, self).__call__(*args, **kwargs) # type: ignore + # pylint: enable=assigning-non-slot, abstract-method + + +# pylint: disable=function-redefined, unused-argument +@typing.overload +def logwrap( + func: None = None, + *, + log: logging.Logger = logger, + log_level: int = logging.DEBUG, + exc_level: int = logging.ERROR, + max_indent: int = 20, + spec: typing.Optional[typing.Callable] = None, + blacklisted_names: typing.Optional[typing.List[str]] = None, + blacklisted_exceptions: typing.Optional[typing.List[typing.Type[Exception]]] = None, + log_call_args: bool = True, + log_call_args_on_exc: bool = True, + log_result_obj: bool = True +) -> LogWrap: + """Overload: with no func.""" + pass + + +@typing.overload # noqa: F811 +def logwrap( + func: typing.Callable, + *, + log: logging.Logger = logger, + log_level: int = logging.DEBUG, + exc_level: int = logging.ERROR, + max_indent: int = 20, + spec: typing.Optional[typing.Callable] = None, + blacklisted_names: typing.Optional[typing.List[str]] = None, + blacklisted_exceptions: typing.Optional[typing.List[typing.Type[Exception]]] = None, + log_call_args: bool = True, + log_call_args_on_exc: bool = True, + log_result_obj: bool = True +) -> typing.Callable: + """Overload: func provided.""" + pass + + +# pylint: enable=unused-argument +def logwrap( # noqa: F811 # pylint: disable=unexpected-keyword-arg, no-value-for-parameter + func: typing.Optional[typing.Callable] = None, + *, + log: logging.Logger = logger, + log_level: int = logging.DEBUG, + exc_level: int = logging.ERROR, + max_indent: int = 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, + log_call_args: bool = True, + log_call_args_on_exc: bool = True, + log_result_obj: bool = True +) -> typing.Union[LogWrap, typing.Callable]: + """Log function calls and return values. Python 3.4+ version. + + :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_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 + """ + 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_result_obj=log_result_obj + ) + if func is not None: + return wrapper(func) + return wrapper +# pylint: enable=function-redefined diff --git a/logwrap/_log_wrap2.py b/logwrap/_log_wrap2.py deleted file mode 100644 index 7017ddc..0000000 --- a/logwrap/_log_wrap2.py +++ /dev/null @@ -1,205 +0,0 @@ -# Copyright 2016-2018 Alexey Stepanov aka penguinolog - -# Copyright 2016 Mirantis, Inc. - -# 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 module. - -This is no reason to import this submodule directly, all required methods is -available from the main module. -""" - -from __future__ import absolute_import -from __future__ import unicode_literals - -import logging -import typing # noqa # pylint: disable=unused-import - -import six -# noinspection PyUnresolvedReferences -import funcsigs - -from . import _log_wrap_shared - -__all__ = ('logwrap', 'LogWrap') - - -class LogWrap(_log_wrap_shared.BaseLogWrap): - """LogWrap.""" - - __slots__ = () - - def __init__( - self, - func=None, # type: typing.Optional[typing.Callable] - log=_log_wrap_shared.logger, # type: logging.Logger - log_level=logging.DEBUG, # type: int - exc_level=logging.ERROR, # type: int - max_indent=20, # type: int - spec=None, # type: typing.Optional[typing.Callable] - blacklisted_names=None, # type: typing.Optional[typing.List[str]] - blacklisted_exceptions=None, # type: typing.Optional[typing.List[typing.Type[Exception]]] - log_call_args=True, # type: bool - log_call_args_on_exc=True, # type: bool - log_result_obj=True, # type: bool - ): # type: (...) -> None - """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_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. - """ - super(LogWrap, self).__init__( - func=func, - 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_result_obj=log_result_obj - ) - - def _get_function_wrapper( - self, - func # type: typing.Callable - ): # type: (...) -> typing.Callable - """Here should be constructed and returned real decorator. - - :param func: Wrapped function - :type func: typing.Callable - :return: wrapped function - :rtype: typing.Callable - """ - sig = funcsigs.signature(obj=self._spec or func) - - # pylint: disable=missing-docstring - # noinspection PyMissingOrEmptyDocstring - @six.wraps(func) - def wrapper(*args, **kwargs): - args_repr = self._get_func_args_repr( - sig=sig, - args=args, - kwargs=kwargs, - ) - - self._make_calling_record(name=func.__name__, arguments=args_repr) - try: - 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 - - # pylint: enable=missing-docstring - return wrapper - - -# pylint: disable=unexpected-keyword-arg, no-value-for-parameter -def logwrap( - func=None, # type: typing.Optional[typing.Callable] - log=_log_wrap_shared.logger, # type: logging.Logger - log_level=logging.DEBUG, # type: int - exc_level=logging.ERROR, # type: int - max_indent=20, # type: int - spec=None, # type: typing.Optional[typing.Callable] - blacklisted_names=None, # type: typing.Optional[typing.List[str]] - blacklisted_exceptions=None, # type: typing.Optional[typing.List[typing.Type[Exception]]] - log_call_args=True, # type: bool - log_call_args_on_exc=True, # type: bool - log_result_obj=True, # type: bool -): # type: (...) -> typing.Union[LogWrap, typing.Callable] - """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.List[str]] - :param blacklisted_exceptions: list of exception, which should be re-raised without producing log record. - :type blacklisted_exceptions: typing.Optional[typing.List[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_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. - """ - 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_result_obj=log_result_obj - ) - if func is not None: - return wrapper(func) - return wrapper -# pylint: enable=unexpected-keyword-arg, no-value-for-parameter diff --git a/logwrap/_log_wrap2.pyi b/logwrap/_log_wrap2.pyi deleted file mode 100644 index 368bbc0..0000000 --- a/logwrap/_log_wrap2.pyi +++ /dev/null @@ -1,55 +0,0 @@ -import logging -import typing -from . import _log_wrap_shared - -class LogWrap(_log_wrap_shared.BaseLogWrap): - - __slots__ = () - - def __init__( - self, - func: typing.Optional[typing.Callable] = None, - log: logging.Logger = ..., - log_level: int = ..., - exc_level: int = ..., - max_indent: int = ..., - spec: typing.Optional[typing.Callable] = ..., - blacklisted_names: typing.Optional[typing.List[str]] = ..., - blacklisted_exceptions: typing.Optional[typing.List[typing.Type[Exception]]] = ..., - log_call_args: bool = ..., - log_call_args_on_exc: bool = ..., - log_result_obj: bool = ..., - ) -> None: ... - - def _get_function_wrapper(self, func: typing.Callable) -> typing.Callable: ... - - -@typing.overload -def logwrap( - func: None = ..., - log: logging.Logger = ..., - log_level: int = ..., - exc_level: int = ..., - max_indent: int = ..., - spec: typing.Optional[typing.Callable] = ..., - blacklisted_names: typing.Optional[typing.List[str]] = ..., - blacklisted_exceptions: typing.Optional[typing.List[typing.Type[Exception]]] = ..., - log_call_args: bool = ..., - log_call_args_on_exc: bool = ..., - log_result_obj: bool = ..., -) -> LogWrap: ... - -@typing.overload -def logwrap( - func: typing.Callable = ..., - log: logging.Logger = ..., - log_level: int = ..., - exc_level: int = ..., - max_indent: int = ..., - spec: typing.Optional[typing.Callable] = ..., - blacklisted_names: typing.Optional[typing.List[str]] = ..., - blacklisted_exceptions: typing.Optional[typing.List[typing.Type[Exception]]] = ..., - log_call_args: bool = ..., - log_call_args_on_exc: bool = ..., - log_result_obj: bool = ..., -) -> typing.Callable: ... diff --git a/logwrap/_log_wrap3.py b/logwrap/_log_wrap3.py deleted file mode 100644 index 967c6b1..0000000 --- a/logwrap/_log_wrap3.py +++ /dev/null @@ -1,241 +0,0 @@ -# Copyright 2016-2018 Alexey Stepanov aka penguinolog - -# Copyright 2016 Mirantis, Inc. - -# 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: async part (python 3.4+). - -This is no reason to import this submodule directly, all required methods is -available from the main module. -""" - -from __future__ import absolute_import -from __future__ import unicode_literals - -# noinspection PyCompatibility -import asyncio -import functools -import inspect -import logging -import typing - -from . import _log_wrap_shared - -__all__ = ('logwrap', 'LogWrap') - - -class LogWrap(_log_wrap_shared.BaseLogWrap): - """Python 3.4+ version of LogWrap.""" - - __slots__ = () - - def __init__( - self, - func: typing.Optional[typing.Callable] = None, - *, - log: logging.Logger = _log_wrap_shared.logger, - log_level: int = logging.DEBUG, - exc_level: int = logging.ERROR, - max_indent: int = 20, - spec: typing.Optional[typing.Callable] = None, - blacklisted_names: typing.Optional[typing.List[str]] = None, - blacklisted_exceptions: typing.Optional[typing.List[typing.Type[Exception]]] = None, - log_call_args: bool = True, - log_call_args_on_exc: bool = True, - log_result_obj: bool = True - ) -> None: - """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_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:: 3.3.0 Deprecation of *args - .. versionchanged:: 4.0.0 Drop of *args - """ - super(LogWrap, self).__init__( - func=func, - 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_result_obj=log_result_obj - ) - - def _get_function_wrapper( - self, - func: typing.Callable - ) -> typing.Callable: - """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) - - # pylint: disable=missing-docstring - # noinspection PyCompatibility,PyMissingOrEmptyDocstring - @functools.wraps(func) - @asyncio.coroutine - def async_wrapper(*args, **kwargs): - 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 - - # noinspection PyCompatibility,PyMissingOrEmptyDocstring - @functools.wraps(func) - def wrapper(*args, **kwargs): - 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 - - # pylint: enable=missing-docstring - return async_wrapper if asyncio.iscoroutinefunction(func) else wrapper - - -# pylint: disable=unexpected-keyword-arg, no-value-for-parameter -def logwrap( - func: typing.Optional[typing.Callable] = None, - *, - log: logging.Logger = _log_wrap_shared.logger, - log_level: int = logging.DEBUG, - exc_level: int = logging.ERROR, - max_indent: int = 20, - spec: typing.Optional[typing.Callable] = None, - blacklisted_names: typing.Optional[typing.List[str]] = None, - blacklisted_exceptions: typing.Optional[typing.List[typing.Type[Exception]]] = None, - log_call_args: bool = True, - log_call_args_on_exc: bool = True, - log_result_obj: bool = True -) -> typing.Union[LogWrap, typing.Callable]: - """Log function calls and return values. Python 3.4+ version. - - :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_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 - """ - 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_result_obj=log_result_obj - ) - if func is not None: - return wrapper(func) - return wrapper -# pylint: enable=unexpected-keyword-arg, no-value-for-parameter diff --git a/logwrap/_log_wrap3.pyi b/logwrap/_log_wrap3.pyi deleted file mode 100644 index 248681e..0000000 --- a/logwrap/_log_wrap3.pyi +++ /dev/null @@ -1,58 +0,0 @@ -import logging -import typing -from . import _log_wrap_shared - -class LogWrap(_log_wrap_shared.BaseLogWrap): - - __slots__ = () - - def __init__( - self, - func: typing.Optional[typing.Callable] = ..., - *, - log: logging.Logger = ..., - log_level: int = ..., - exc_level: int = ..., - max_indent: int = ..., - spec: typing.Optional[typing.Callable] = ..., - blacklisted_names: typing.Optional[typing.List[str]] = ..., - blacklisted_exceptions: typing.Optional[typing.List[typing.Type[Exception]]] = ..., - log_call_args: bool = ..., - log_call_args_on_exc: bool = ..., - log_result_obj: bool = ... - ) -> None: ... - - def _get_function_wrapper(self, func: typing.Callable) -> typing.Callable: ... - - -@typing.overload -def logwrap( - func: None = ..., - *, - log: logging.Logger = ..., - log_level: int = ..., - exc_level: int = ..., - max_indent: int = ..., - spec: typing.Optional[typing.Callable] = ..., - blacklisted_names: typing.Optional[typing.List[str]] = ..., - blacklisted_exceptions: typing.Optional[typing.List[typing.Type[Exception]]] = ..., - log_call_args: bool = ..., - log_call_args_on_exc: bool = ..., - log_result_obj: bool = ... -) -> LogWrap: ... - -@typing.overload -def logwrap( - func: typing.Callable = ..., - *, - log: logging.Logger = ..., - log_level: int = ..., - exc_level: int = ..., - max_indent: int = ..., - spec: typing.Optional[typing.Callable] = ..., - blacklisted_names: typing.Optional[typing.List[str]] = ..., - blacklisted_exceptions: typing.Optional[typing.List[typing.Type[Exception]]] = ..., - log_call_args: bool = ..., - log_call_args_on_exc: bool = ..., - log_result_obj: bool = ... -) -> typing.Callable: ... diff --git a/logwrap/_log_wrap_shared.pyi b/logwrap/_log_wrap_shared.pyi deleted file mode 100644 index d37c55a..0000000 --- a/logwrap/_log_wrap_shared.pyi +++ /dev/null @@ -1,169 +0,0 @@ -import abc -import enum -import inspect -import logging -import typing - -import six - -from . import _class_decorator - -if six.PY3: - from inspect import Parameter - from inspect import Signature -else: - from funcsigs import Parameter - from funcsigs import Signature - -logger: logging.Logger - -def _check_type(expected: typing.Type) -> typing.Callable: ... - - -class BoundParameter(object): - - __slots__ = ("_parameter", "_value") - - POSITIONAL_ONLY = Parameter.POSITIONAL_ONLY - POSITIONAL_OR_KEYWORD = Parameter.POSITIONAL_OR_KEYWORD - VAR_POSITIONAL = Parameter.VAR_POSITIONAL - KEYWORD_ONLY = Parameter.KEYWORD_ONLY - VAR_KEYWORD = Parameter.VAR_KEYWORD - - empty: typing.Type = Parameter.empty - - def __init__(self, parameter: Parameter, value: typing.Any = ...) -> None: ... - - @property - def parameter(self) -> Parameter: ... - - @property - def name(self) -> typing.Union[None, str]: ... - - @property - def default(self) -> typing.Any: ... - - @property - def annotation(self) -> typing.Union[Parameter.empty, str]: ... - - @property - def kind(self) -> enum.IntEnum: ... - - @property - def value(self) -> typing.Any: ... - - -def bind_args_kwargs( - sig: Signature, - *args: typing.Tuple, - **kwargs: typing.Dict -) -> typing.Iterator[BoundParameter]: ... - - -class BaseLogWrap(_class_decorator.BaseDecorator, metaclass=abc.ABCMeta): - def __init__( - self, - func: typing.Optional[typing.Callable] = None, - log: logging.Logger = ..., - log_level: int = ..., - exc_level: int = ..., - max_indent: int = ..., - spec: typing.Optional[typing.Callable] = ..., - blacklisted_names: typing.Optional[typing.Iterable[str]] = ..., - blacklisted_exceptions: typing.Optional[typing.Iterable[Exception]] = ..., - log_call_args: bool = ..., - log_call_args_on_exc: bool = ..., - log_result_obj: bool = ..., - ) -> None: ... - - @property - def log_level(self) -> int: ... - - @log_level.setter - def log_level(self, val: int) -> None: ... - - @property - def exc_level(self) -> int: ... - - @exc_level.setter - def exc_level(self, val: int) -> None: ... - - @property - def max_indent(self) -> int: ... - - @max_indent.setter - def max_indent(self, val: int) -> None: ... - - @property - def blacklisted_names(self) -> typing.List[str]: ... - - @property - def blacklisted_exceptions(self) -> typing.List[typing.Type[Exception]]: ... - - @property - def log_call_args(self) -> bool: ... - - @log_call_args.setter - def log_call_args(self, val: bool) -> None: ... - - @property - def log_call_args_on_exc(self) -> bool: ... - - @log_call_args_on_exc.setter - def log_call_args_on_exc(self, val: bool) -> None: ... - - @property - def log_result_obj(self) -> bool: ... - - @log_result_obj.setter - def log_result_obj(self, val: bool) -> None: ... - - @property - def _logger(self) -> logging.Logger: ... - - @property - def _spec(self) -> typing.Callable: ... - - @staticmethod - def _bind_args_kwargs( - sig: Signature, - *args: typing.Tuple, - **kwargs: typing.Dict - ) -> typing.Iterator[BoundParameter]: ... - - def pre_process_param( - self, - arg: BoundParameter - ) -> typing.Union[BoundParameter, typing.Tuple[BoundParameter, typing.Any], None]: ... - - def post_process_param( - self, - arg: BoundParameter, - arg_repr: typing.Text - ) -> typing.Text: ... - - def _get_func_args_repr( - self, - sig: inspect.Signature, - args: typing.Tuple, - kwargs: typing.Dict[str, typing.Any] - ) -> typing.Text: ... - - def _make_done_record( - self, - func_name: str, - result: typing.Any - ) -> None: ... - - def _make_calling_record( - self, - name: str, - arguments: str, - method: str = "Calling" - ) -> None: ... - - def _make_exc_record( - self, - name: str, - arguments: str - ) -> None: ... diff --git a/logwrap/_repr_utils.py b/logwrap/_repr_utils.py index 3683b06..c33bec2 100644 --- a/logwrap/_repr_utils.py +++ b/logwrap/_repr_utils.py @@ -20,41 +20,23 @@ available from the main module. """ -from __future__ import absolute_import -from __future__ import unicode_literals - import abc +import inspect import types -import typing # noqa # pylint: disable=unused-import - -import six - -# pylint: disable=ungrouped-imports, no-name-in-module -if six.PY3: # pragma: no cover - from inspect import ( - Parameter, - signature, - ) -else: # pragma: no cover - # noinspection PyUnresolvedReferences - from funcsigs import ( - Parameter, - signature, - ) -# pylint: enable=ungrouped-imports, no-name-in-module +import typing -def _known_callable(item): # type: (typing.Any) -> bool +def _known_callable(item: typing.Any) -> bool: """Check for possibility to parse callable.""" return isinstance(item, (types.FunctionType, types.MethodType)) -def _simple(item): # type: (typing.Any) -> bool +def _simple(item: typing.Any) -> bool: """Check for nested iterations: True, if not.""" return not isinstance(item, (list, set, tuple, dict, frozenset)) -class ReprParameter(object): +class ReprParameter: """Parameter wrapper wor repr and str operations over signature.""" __slots__ = ( @@ -62,19 +44,19 @@ class ReprParameter(object): '_parameter' ) - POSITIONAL_ONLY = Parameter.POSITIONAL_ONLY - POSITIONAL_OR_KEYWORD = Parameter.POSITIONAL_OR_KEYWORD - VAR_POSITIONAL = Parameter.VAR_POSITIONAL - KEYWORD_ONLY = Parameter.KEYWORD_ONLY - VAR_KEYWORD = Parameter.VAR_KEYWORD + 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 = Parameter.empty + empty = inspect.Parameter.empty def __init__( self, - parameter, # type: Parameter - value=None # type: typing.Optional[typing.Any] - ): # type: (...) -> None + parameter: inspect.Parameter, + value: typing.Optional[typing.Any] = None + ) -> None: """Parameter-like object store for repr and str tasks. :param parameter: parameter from signature @@ -86,24 +68,24 @@ def __init__( self._value = value if value is not None else parameter.default @property - def parameter(self): # type: () -> Parameter + def parameter(self) -> inspect.Parameter: """Parameter object.""" return self._parameter @property - def name(self): # type: () -> typing.Union[None, str] + def name(self) -> typing.Union[None, str]: """Parameter name. For `*args` and `**kwargs` add prefixes """ - if self.kind == Parameter.VAR_POSITIONAL: + if self.kind == inspect.Parameter.VAR_POSITIONAL: return '*' + self.parameter.name - elif self.kind == Parameter.VAR_KEYWORD: + if self.kind == inspect.Parameter.VAR_KEYWORD: return '**' + self.parameter.name return self.parameter.name @property - def value(self): # type: () -> typing.Any + def value(self) -> typing.Any: """Parameter value to log. If function is bound to class -> value is class instance else default value. @@ -111,16 +93,16 @@ def value(self): # type: () -> typing.Any return self._value @property - def annotation(self): # type: () -> typing.Union[Parameter.empty, str] + def annotation(self) -> typing.Union[inspect.Parameter.empty, str]: """Parameter annotation.""" return self.parameter.annotation @property - def kind(self): # type: () -> int + def kind(self) -> int: """Parameter kind.""" - return self.parameter.kind + return self.parameter.kind # type: ignore - def __hash__(self): # pragma: no cover + def __hash__(self) -> int: # pragma: no cover """Block hashing. :raises TypeError: Not hashable. @@ -128,29 +110,32 @@ def __hash__(self): # pragma: no cover msg = "unhashable type: '{0}'".format(self.__class__.__name__) raise TypeError(msg) - def __repr__(self): + def __repr__(self) -> str: """Debug purposes.""" return '<{} "{}">'.format(self.__class__.__name__, self) # pylint: disable=no-member def _prepare_repr( - func # type: typing.Union[types.FunctionType, types.MethodType] -): # type: (...) -> typing.Iterator[ReprParameter] + func: typing.Union[types.FunctionType, types.MethodType] +) -> typing.Iterator[ReprParameter]: """Get arguments lists with defaults. :type func: typing.Union[types.FunctionType, types.MethodType] :rtype: typing.Iterator[ReprParameter] """ isfunction = isinstance(func, types.FunctionType) - real_func = func if isfunction else func.__func__ # type: typing.Callable + if isfunction: + real_func = func + else: + real_func = func.__func__ # type: ignore - parameters = list(signature(real_func).parameters.values()) + parameters = list(inspect.signature(real_func).parameters.values()) params = iter(parameters) - if not isfunction and func.__self__ is not None: + if not isfunction and func.__self__ is not None: # type: ignore try: - yield ReprParameter(next(params), value=func.__self__) + yield ReprParameter(next(params), value=func.__self__) # type: ignore except StopIteration: # pragma: no cover return for arg in params: @@ -158,7 +143,7 @@ def _prepare_repr( # pylint: enable=no-member -class PrettyFormat(object): +class PrettyFormat: """Pretty Formatter. Designed for usage as __repr__ and __str__ replacement on complex objects @@ -167,30 +152,25 @@ class PrettyFormat(object): __slots__ = ( '__max_indent', '__indent_step', - '__py2_str', ) def __init__( self, - max_indent=20, # type: int - indent_step=4, # type: int - py2_str=False, # type: bool - ): # type: (...) -> None + max_indent: int = 20, + indent_step: int = 4, + ) -> None: """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 - :param py2_str: use Python 2.x compatible strings instead of unicode - :type py2_str: bool """ self.__max_indent = max_indent self.__indent_step = indent_step - self.__py2_str = py2_str and not six.PY3 # Python 2 only behavior @property - def max_indent(self): # type: () -> int + def max_indent(self) -> int: """Max indent getter. :rtype: int @@ -198,14 +178,14 @@ def max_indent(self): # type: () -> int return self.__max_indent @property - def indent_step(self): # type: () -> int + def indent_step(self) -> int: """Indent step getter. :rtype: int """ return self.__indent_step - def next_indent(self, indent, multiplier=1): # type: (int, int) -> int + def next_indent(self, indent: int, multiplier: int = 1) -> int: """Next indentation value. :param indent: current indentation value @@ -219,61 +199,58 @@ def next_indent(self, indent, multiplier=1): # type: (int, int) -> int @abc.abstractmethod def _repr_callable( self, - src, # type: typing.Union[types.FunctionType, types.MethodType] - indent=0 # type: int - ): # type: (...) -> typing.Text + src: typing.Union[types.FunctionType, types.MethodType], + indent: int = 0 + ) -> str: """Repr callable object (function or method). :type src: typing.Union[types.FunctionType, types.MethodType] :type indent: int - :rtype: typing.Text + :rtype: str """ raise NotImplementedError() # pragma: no cover @abc.abstractmethod def _repr_simple( self, - src, # type: typing.Any - indent=0, # type: int - no_indent_start=False # type: bool - ): # type: (...) -> typing.Text + src: typing.Any, + indent: int = 0, + no_indent_start: bool = False + ) -> str: """Repr object without iteration. - :type src: typing.Union[ - typing.AnyStr, int, typing.Iterable, - object, - ] + :type src: typing.Any :type indent: int :type no_indent_start: bool - :rtype: typing.Text + :rtype: str """ raise NotImplementedError() # pragma: no cover @abc.abstractmethod def _repr_dict_items( self, - src, # type: typing.Dict - indent=0 # type: int - ): # type: (...) -> typing.Iterator[typing.Text] + src: typing.Dict, + indent: int = 0 # type + ) -> typing.Iterator[str]: """Repr dict items. :param src: object to process :type src: typing.Dict :param indent: start indentation :type indent: int - :rtype: typing.Iterator[typing.Text] + :rtype: typing.Iterator[str] """ raise NotImplementedError() # pragma: no cover @staticmethod def _repr_iterable_item( - nl, # type: bool - obj_type, # type: str - prefix, # type: str - indent, # type: int - result, # type: str - suffix, # type: str - ): # type: (...) -> typing.Text + nl: bool, + obj_type: str, + prefix: str, + indent: int, + result: str, + suffix: str + ) -> str: """Repr iterable item. :param nl: newline before item @@ -288,22 +265,22 @@ def _repr_iterable_item( :type result: str :param suffix: suffix :type suffix: str - :rtype: typing.Text + :rtype: str """ raise NotImplementedError() # pragma: no cover def _repr_iterable_items( self, - src, # type: typing.Iterable - indent=0 # type: int - ): # type: (...) -> typing.Iterator[typing.Text] + src: typing.Iterable, + indent: int = 0 + ) -> typing.Iterator[str]: """Repr iterable items (not designed for dicts). :param src: object to process :type src: typing.Iterable :param indent: start indentation :type indent: int - :rtype: typing.Iterator[typing.Text] + :rtype: typing.Iterator[str] """ for elem in src: yield '\n' + self.process_element( @@ -313,35 +290,32 @@ def _repr_iterable_items( @property @abc.abstractmethod - def _magic_method_name(self): # type: () -> typing.Text + def _magic_method_name(self) -> str: """Magic method name. - :rtype: typing.Text + :rtype: str """ raise NotImplementedError() # pragma: no cover def process_element( self, - src, # type: typing.Any - indent=0, # type: int - no_indent_start=False # type: bool - ): # type: (...) -> typing.Text + src: typing.Any, + indent: int = 0, + no_indent_start: bool = False + ) -> str: """Make human readable representation of object. :param src: object to process - :type src: typing.Union[ - typing.AnyStr, int, typing.Iterable, object - ] + :type src: typing.Any :param indent: start indentation :type indent: int - :param no_indent_start: - do not indent open bracket and simple parameters + :param no_indent_start: do not indent open bracket and simple parameters :type no_indent_start: bool :return: formatted string - :rtype: typing.Text + :rtype: str """ if hasattr(src, self._magic_method_name): - return getattr( + result = getattr( src, self._magic_method_name )( @@ -349,6 +323,7 @@ def process_element( indent=indent, no_indent_start=no_indent_start ) + return result # type: ignore if _known_callable(src): return self._repr_callable( @@ -387,20 +362,17 @@ def process_element( def __call__( self, - src, # type: typing.Any - indent=0, # type: int - no_indent_start=False # type: bool - ): # type: (...) -> typing.Union[six.text_type, str] + src: typing.Any, + indent: int = 0, + no_indent_start: bool = False + ) -> str: """Make human readable representation of object. The main entry point. :param src: object to process - :type src: typing.Union[ - typing.AnyStr, int, typing.Iterable, object - ] + :type src: typing.Any :param indent: start indentation :type indent: int - :param no_indent_start: - do not indent open bracket and simple parameters + :param no_indent_start: do not indent open bracket and simple parameters :type no_indent_start: bool :return: formatted string :rtype: str @@ -410,11 +382,6 @@ def __call__( indent=indent, no_indent_start=no_indent_start ) - if self.__py2_str: # pragma: no cover - return result.encode( - encoding='utf-8', - errors='backslashreplace', - ) return result @@ -427,24 +394,21 @@ class PrettyRepr(PrettyFormat): __slots__ = () @property - def _magic_method_name(self): # type: () -> typing.Text + def _magic_method_name(self) -> str: """Magic method name. - :rtype: typing.Text + :rtype: str """ return '__pretty_repr__' @staticmethod def _strings_repr( - indent, # type: int - val # type: typing.AnyStr - ): # type: (...) -> typing.Text + indent: int, + val: typing.Union[bytes, str] + ) -> str: """Custom repr for strings and binary strings.""" - if isinstance(val, six.binary_type): - val = val.decode( - encoding='utf-8', - errors='backslashreplace' - ) + if isinstance(val, bytes): + val = val.decode(encoding='utf-8', errors='backslashreplace') prefix = 'b' else: prefix = 'u' @@ -457,19 +421,16 @@ def _strings_repr( def _repr_simple( self, - src, # type: typing.Any - indent=0, # type: int - no_indent_start=False # type: bool - ): # type: (...) -> typing.Text + src: typing.Any, + indent: int = 0, + no_indent_start: bool = False + ) -> str: """Repr object without iteration. - :type src: typing.Union[ - typing.AnyStr, int, typing.Iterable, - object, - ] + :type src: typing.Any :type indent: int :type no_indent_start: bool - :rtype: typing.Text + :rtype: str """ indent = 0 if no_indent_start else indent if isinstance(src, set): @@ -478,7 +439,7 @@ def _repr_simple( indent=indent, val="set(" + ' ,'.join(map(repr, src)) + ")" ) - if isinstance(src, (six.binary_type, six.text_type)): + if isinstance(src, (bytes, str)): return self._strings_repr(indent=indent, val=src) return "{spc:<{indent}}{val!r}".format( spc='', @@ -488,16 +449,16 @@ def _repr_simple( def _repr_dict_items( self, - src, # type: typing.Dict - indent=0 # type: int - ): # type: (...) -> typing.Iterator[typing.Text] + src: typing.Dict, + indent: int = 0 + ) -> typing.Iterator[str]: """Repr dict items. :param src: object to process :type src: dict :param indent: start indentation :type indent: int - :rtype: typing.Iterator[typing.Text] + :rtype: typing.Iterator[str] """ max_len = max((len(repr(key)) for key in src)) if src else 0 for key, val in src.items(): @@ -515,14 +476,14 @@ def _repr_dict_items( def _repr_callable( self, - src, # type: typing.Union[types.FunctionType, types.MethodType] - indent=0 # type: int - ): # type: (...) -> typing.Text + src: typing.Union[types.FunctionType, types.MethodType], + indent: int = 0 + ) -> str: """Repr callable object (function or method). :type src: typing.Union[types.FunctionType, types.MethodType] :type indent: int - :rtype: typing.Text + :rtype: str """ param_str = "" @@ -547,8 +508,11 @@ def _repr_callable( if param_str: param_str += "\n" + " " * indent - sig = signature(src) - annotation = '' if sig.return_annotation == Parameter.empty else ' -> {sig.return_annotation!r}'.format(sig=sig) + sig = inspect.signature(src) + if sig.return_annotation == 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="", @@ -560,13 +524,13 @@ def _repr_callable( @staticmethod def _repr_iterable_item( - nl, # type: bool - obj_type, # type: str - prefix, # type: str - indent, # type: int - result, # type: str - suffix, # type: str - ): # type: (...) -> typing.Text + nl: bool, + obj_type: str, + prefix: str, + indent: int, + result: str, + suffix: str, + ) -> str: """Repr iterable item. :param nl: newline before item @@ -581,7 +545,7 @@ def _repr_iterable_item( :type result: str :param suffix: suffix :type suffix: str - :rtype: typing.Text + :rtype: str """ return ( "{nl}" @@ -607,24 +571,21 @@ class PrettyStr(PrettyFormat): __slots__ = () @property - def _magic_method_name(self): # type: () -> typing.Text + def _magic_method_name(self) -> str: """Magic method name. - :rtype: typing.Text + :rtype: str """ return '__pretty_str__' @staticmethod def _strings_str( - indent, # type: int - val # type: typing.AnyStr - ): # type: (...) -> typing.Text + indent: int, + val: typing.Union[bytes, str] + ) -> str: """Custom repr for strings and binary strings.""" - if isinstance(val, six.binary_type): - val = val.decode( - encoding='utf-8', - errors='backslashreplace' - ) + if isinstance(val, bytes): + val = val.decode(encoding='utf-8', errors='backslashreplace') return "{spc:<{indent}}{string}".format( spc='', indent=indent, @@ -633,19 +594,16 @@ def _strings_str( def _repr_simple( self, - src, # type: typing.Any - indent=0, # type: int - no_indent_start=False # type: bool - ): # type: (...) -> typing.Text + src: typing.Any, + indent: int = 0, + no_indent_start: bool = False + ) -> str: """Repr object without iteration. - :type src: typing.Union[ - typing.AnyStr, int, typing.Iterable, - object, - ] + :type src: typing.Any :type indent: int :type no_indent_start: bool - :rtype: typing.Text + :rtype: str """ indent = 0 if no_indent_start else indent if isinstance(src, set): @@ -654,7 +612,7 @@ def _repr_simple( indent=indent, val="set(" + ' ,'.join(map(str, src)) + ")" ) - if isinstance(src, (six.binary_type, six.text_type)): + if isinstance(src, (bytes, str)): return self._strings_str(indent=indent, val=src) return "{spc:<{indent}}{val!s}".format( spc='', @@ -664,16 +622,16 @@ def _repr_simple( def _repr_dict_items( self, - src, # type: typing.Dict - indent=0 # type: int - ): # type: (...) -> typing.Iterator[typing.Text] + src: typing.Dict, + indent: int = 0 + ) -> typing.Iterator[str]: """Repr dict items. :param src: object to process :type src: dict :param indent: start indentation :type indent: int - :rtype: typing.Generator[typing.Text] + :rtype: typing.Generator[str] """ max_len = max((len(str(key)) for key in src)) if src else 0 for key, val in src.items(): @@ -691,14 +649,14 @@ def _repr_dict_items( def _repr_callable( self, - src, # type: typing.Union[types.FunctionType, types.MethodType] - indent=0 # type: int - ): # type: (...) -> typing.Text + src: typing.Union[types.FunctionType, types.MethodType], + indent: int = 0 + ) -> str: """Repr callable object (function or method). :type src: typing.Union[types.FunctionType, types.MethodType] :type indent: int - :rtype: typing.Text + :rtype: str """ param_str = "" @@ -723,8 +681,11 @@ def _repr_callable( if param_str: param_str += "\n" + " " * indent - sig = signature(src) - annotation = '' if sig.return_annotation == Parameter.empty else ' -> {sig.return_annotation!r}'.format(sig=sig) + sig = inspect.signature(src) + if sig.return_annotation == 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="", @@ -736,13 +697,13 @@ def _repr_callable( @staticmethod def _repr_iterable_item( - nl, # type: bool - obj_type, # type: str - prefix, # type: str - indent, # type: int - result, # type: str - suffix, # type: str - ): # type: (...) -> typing.Text + nl: bool, + obj_type: str, + prefix: str, + indent: int, + result: str, + suffix: str + ) -> str: """Repr iterable item. :param nl: newline before item @@ -757,7 +718,7 @@ def _repr_iterable_item( :type result: str :param suffix: suffix :type suffix: str - :rtype: typing.Text + :rtype: str """ return ( "{nl}" @@ -774,19 +735,16 @@ def _repr_iterable_item( def pretty_repr( - src, # type: typing.Any - indent=0, # type: int - no_indent_start=False, # type: bool - max_indent=20, # type: int - indent_step=4, # type: int - py2_str=False, # type: bool -): # type: (...) -> typing.Union[six.text_type, str] + src: typing.Any, + indent: int = 0, + no_indent_start: bool = False, + max_indent: int = 20, + indent_step: int = 4, +) -> str: """Make human readable repr of object. :param src: object to process - :type src: typing.Union[ - typing.AnyStr, int, typing.Iterable, object - ] + :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 @@ -795,15 +753,12 @@ def pretty_repr( :type max_indent: int :param indent_step: step for the next indentation level :type indent_step: int - :param py2_str: use Python 2.x compatible strings instead of unicode - :type py2_str: bool :return: formatted string :rtype: str """ return PrettyRepr( max_indent=max_indent, indent_step=indent_step, - py2_str=py2_str )( src=src, indent=indent, @@ -812,19 +767,16 @@ def pretty_repr( def pretty_str( - src, # type: typing.Any - indent=0, # type: int - no_indent_start=False, # type: bool - max_indent=20, # type: int - indent_step=4, # type: int - py2_str=False, # type: bool -): # type: (...) -> typing.Union[six.text_type, str] + src: typing.Any, + indent: int = 0, + no_indent_start: bool = False, + max_indent: int = 20, + indent_step: int = 4, +) -> str: """Make human readable str of object. :param src: object to process - :type src: typing.Union[ - typing.AnyStr, int, typing.Iterable, object - ] + :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 @@ -833,14 +785,11 @@ def pretty_str( :type max_indent: int :param indent_step: step for the next indentation level :type indent_step: int - :param py2_str: use Python 2.x compatible strings instead of unicode - :type py2_str: bool :return: formatted string """ return PrettyStr( max_indent=max_indent, indent_step=indent_step, - py2_str=py2_str )( src=src, indent=indent, diff --git a/logwrap/_repr_utils.pyi b/logwrap/_repr_utils.pyi deleted file mode 100644 index 925595d..0000000 --- a/logwrap/_repr_utils.pyi +++ /dev/null @@ -1,171 +0,0 @@ -import abc -import types -import typing - -import six - -class PrettyFormat: - def __init__( - self, - max_indent: int = ..., - indent_step: int = ..., - py2_str: bool = ... - ) -> None: ... - - @property - def max_indent(self) -> int: ... - - @property - def indent_step(self) -> int: ... - - def next_indent(self, indent: int, multiplier: int = ...) -> int: ... - - @abc.abstractmethod - def _repr_callable( - self, - src: typing.Union[types.FunctionType, types.MethodType], - indent: int = 0 - ) -> typing.Text: ... - - @abc.abstractmethod - def _repr_simple( - self, - src: typing.Any, - indent: int = 0, - no_indent_start: bool = False - ) -> typing.Text: ... - - @abc.abstractmethod - def _repr_dict_items( - self, - src: typing.Dict, - indent: int = 0 - ) -> typing.Iterator[typing.Text]: ... - - @staticmethod - def _repr_iterable_item( - nl: bool, - obj_type: str, - prefix: str, - indent: int, - result: str, - suffix: str - ) -> typing.Text: ... - - def _repr_iterable_items( - self, - src: typing.Iterable, - indent: int = 0 - ) -> typing.Iterator[str]: ... - - @property - @abc.abstractmethod - def _magic_method_name(self) -> typing.Text: ... - - def process_element(self, src: typing.Any, indent: int = ..., no_indent_start: bool = ...) -> typing.Text: ... - - def __call__( - self, - src: typing.Any, - indent: int = ..., - no_indent_start: bool = ... - ) -> typing.Union[six.text_type, str]: ... - - -class PrettyRepr(PrettyFormat): - @property - def _magic_method_name(self) -> typing.Text: ... - - @staticmethod - def _strings_repr( - indent: int, - val: typing.AnyStr - ) -> typing.Text: ... - - def _repr_simple( - self, - src: typing.Any, - indent: int = 0, - no_indent_start: bool = False - ) -> typing.Text: ... - - def _repr_dict_items( - self, - src: typing.Dict, - indent: int = 0 - ) -> typing.Iterator[typing.Text]: ... - - def _repr_callable( - self, - src: typing.Union[types.FunctionType, types.MethodType], - indent: int = 0 - ) -> typing.Text: ... - - @staticmethod - def _repr_iterable_item( - nl: bool, - obj_type: str, - prefix: str, - indent: int, - result: str, - suffix: str - ) -> typing.Text: ... - - -class PrettyStr(PrettyFormat): - @property - def _magic_method_name(self) -> typing.Text: ... - - @staticmethod - def _strings_str( - indent: int, - val: typing.AnyStr - ) -> typing.Text: ... - - def _repr_simple( - self, - src: typing.Any, - indent: int = 0, - no_indent_start: bool = False - ) -> typing.Text: ... - - def _repr_dict_items( - self, - src: typing.Dict, - indent: int = 0 - ) -> typing.Iterator[typing.Text]: ... - - def _repr_callable( - self, - src: typing.Union[types.FunctionType, types.MethodType], - indent: int = 0 - ) -> typing.Text: ... - - @staticmethod - def _repr_iterable_item( - nl: bool, - obj_type: str, - prefix: str, - indent: int, - result: str, - suffix: str - ) -> typing.Text: ... - - -def pretty_repr( - src: typing.Any, - indent: int = ..., - no_indent_start: bool = ..., - max_indent: int = ..., - indent_step: int = ..., - py2_str: bool = ..., -) -> typing.Union[six.text_type, str]: ... - -def pretty_str( - src: typing.Any, - indent: int = ..., - no_indent_start: bool = ..., - max_indent: int = ..., - indent_step: int = ..., - py2_str: bool = ..., -) -> typing.Union[six.text_type, str]: ... diff --git a/requirements.txt b/requirements.txt index f4e03a2..2c2374a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ -six >=1.9 typing >= 3.6 ; python_version < "3.8" diff --git a/setup.py b/setup.py index e4db1de..b3f025d 100644 --- a/setup.py +++ b/setup.py @@ -34,8 +34,6 @@ import setuptools -PY3 = sys.version_info[:2] > (2, 7) - with open( os.path.join( os.path.dirname(__file__), @@ -58,7 +56,7 @@ def _extension(modpath): requires_optimization = [ _extension('logwrap._class_decorator'), - _extension('logwrap._log_wrap_shared'), + _extension('logwrap._log_wrap'), _extension('logwrap._repr_utils'), ] @@ -76,7 +74,7 @@ def _extension(modpath): overflowcheck=True, language_level=3, ) -) if cythonize is not None and PY3 else [] +) if cythonize is not None else [] class BuildFailed(Exception): @@ -101,7 +99,7 @@ def run(self): 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_wrap3.py'), + os.path.join('logwrap', '_log_wrap.py'), ) for src_file in src_files: @@ -178,10 +176,9 @@ def get_simple_vars_from_src(src): """ ast_data = ( ast.Str, ast.Num, - ast.List, ast.Set, ast.Dict, ast.Tuple + ast.List, ast.Set, ast.Dict, ast.Tuple, + ast.Bytes, ast.NameConstant, ) - if PY3: - ast_data += (ast.Bytes, ast.NameConstant,) tree = ast.parse(src) @@ -223,10 +220,7 @@ def get_simple_vars_from_src(src): 'License :: OSI Approved :: Apache Software License', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', @@ -256,7 +250,7 @@ def get_simple_vars_from_src(src): long_description=long_description, classifiers=classifiers, keywords=keywords, - python_requires='>=2.7.5,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*', + python_requires='>=3.5.0', # While setuptools cannot deal with pre-installed incompatible versions, # setting a lower bound is not harmful - it makes error messages cleaner. DO # NOT set an upper bound on setuptools, as that will lead to uninstallable @@ -266,12 +260,6 @@ def get_simple_vars_from_src(src): setup_requires="setuptools >= 21.0.0,!=24.0.0," "!=34.0.0,!=34.0.1,!=34.0.2,!=34.0.3,!=34.1.0,!=34.1.1,!=34.2.0,!=34.3.0,!=34.3.1,!=34.3.2," "!=36.2.0", - extras_require={ - ':python_version == "2.7"': [ - 'funcsigs>=1.0', - 'enum34>=1.1', - ], - }, install_requires=required, package_data={ 'logwrap': [ @@ -282,7 +270,7 @@ def get_simple_vars_from_src(src): ], }, ) -if PY3 and cythonize is not None: +if cythonize is not None: setup_args['ext_modules'] = ext_modules setup_args['cmdclass'] = dict(build_ext=AllowFailRepair) diff --git a/test/test_log_wrap.py b/test/test_log_wrap.py index b6ec27e..6bcbbf1 100644 --- a/test/test_log_wrap.py +++ b/test/test_log_wrap.py @@ -18,22 +18,14 @@ """Python independent logwrap tests.""" -from __future__ import absolute_import -from __future__ import unicode_literals - +import functools +import io import logging import unittest -import six - import logwrap -# pylint: disable=import-error -if six.PY2: - # noinspection PyUnresolvedReferences - import mock -else: - from unittest import mock +from unittest import mock # noinspection PyUnusedLocal,PyMissingOrEmptyDocstring @@ -43,37 +35,19 @@ def setUp(self): Due to no possibility of proper mock patch of function defaults, modify directly. """ - self.logger = mock.Mock(spec=logging.Logger) - if six.PY2: - self.logwrap_defaults = logwrap.logwrap.func_defaults - self.logwrap_cls_defaults = logwrap.LogWrap.__init__.im_func.func_defaults - logwrap.logwrap.func_defaults = logwrap.LogWrap.__init__.im_func.func_defaults = tuple(( - None, - self.logger, - logging.DEBUG, - logging.ERROR, - 20, - None, - None, - None, - True, - True, - True - )) - # raise ValueError(logwrap.logwrap.func_defaults) - else: - self.logwrap_defaults = logwrap.logwrap.__kwdefaults__['log'] - self.logwrap_cls_defaults = logwrap.LogWrap.__init__.__kwdefaults__['log'] - logwrap.logwrap.__kwdefaults__['log'] = logwrap.LogWrap.__init__.__kwdefaults__['log'] = self.logger + self.logger = logging.getLogger('logwrap') + self.logger.setLevel(logging.DEBUG) + + self.stream = io.StringIO() + + self.logger.handlers.clear() + handler = logging.StreamHandler(self.stream) + handler.setFormatter(logging.Formatter(fmt='%(levelname)s>%(message)s')) + self.logger.addHandler(handler) def tearDown(self): """Revert modifications.""" - if six.PY2: - logwrap.logwrap.func_defaults = self.logwrap_defaults - logwrap.LogWrap.__init__.im_func.func_defaults = self.logwrap_cls_defaults - else: - logwrap.LogWrap.__init__.__kwdefaults__['log'] = self.logwrap_cls_defaults - logwrap.logwrap.__kwdefaults__['log'] = self.logwrap_defaults + self.logger.handlers.clear() def test_001_no_args(self): @logwrap.logwrap @@ -82,19 +56,13 @@ def func(): result = func() self.assertEqual(result, 'No args') + self.assertEqual( - [ - mock.call.log( - level=logging.DEBUG, - msg="Calling: \n'func'()" - ), - mock.call.log( - level=logging.DEBUG, - msg="Done: 'func' with result:\n{}".format( - logwrap.pretty_repr(result)) - ), - ], - self.logger.mock_calls, + "DEBUG>Calling: \n" + "'func'()\n" + "DEBUG>Done: 'func' with result:\n" + "u'''No args'''\n", + self.stream.getvalue(), ) def test_002_args_simple(self): @@ -107,30 +75,14 @@ def func(tst): result = func(arg) self.assertEqual(result, arg) self.assertEqual( - [ - mock.call.log( - level=logging.DEBUG, - msg=( - "Calling: \n" - "'func'(\n" - " # POSITIONAL_OR_KEYWORD:\n" - " 'tst'={},\n" - ")".format( - logwrap.pretty_repr( - arg, - indent=8, - no_indent_start=True - ) - ) - ) - ), - mock.call.log( - level=logging.DEBUG, - msg="Done: 'func' with result:\n{}".format( - logwrap.pretty_repr(result)) - ), - ], - self.logger.mock_calls, + "DEBUG>Calling: \n" + "'func'(\n" + " # POSITIONAL_OR_KEYWORD:\n" + " 'tst'=u'''test arg''',\n" + ")\n" + "DEBUG>Done: 'func' with result:\n" + "u'''test arg'''\n", + self.stream.getvalue(), ) def test_003_args_defaults(self): @@ -144,29 +96,14 @@ def func(tst=arg): self.assertEqual(result, arg) self.assertEqual( - [ - mock.call.log( - level=logging.DEBUG, - msg=( - "Calling: \n" - "'func'(\n" - " # POSITIONAL_OR_KEYWORD:\n" - " 'tst'={},\n" - ")".format( - logwrap.pretty_repr( - arg, - indent=8, - no_indent_start=True) - ) - ) - ), - mock.call.log( - level=logging.DEBUG, - msg="Done: 'func' with result:\n{}".format( - logwrap.pretty_repr(result)) - ), - ], - self.logger.mock_calls, + "DEBUG>Calling: \n" + "'func'(\n" + " # POSITIONAL_OR_KEYWORD:\n" + " 'tst'=u'''test arg''',\n" + ")\n" + "DEBUG>Done: 'func' with result:\n" + "u'''test arg'''\n", + self.stream.getvalue(), ) def test_004_args_complex(self): @@ -181,32 +118,23 @@ def func(param_string, param_dictionary): self.assertEqual(result, (string, dictionary)) self.assertEqual( - [ - mock.call.log( - level=logging.DEBUG, - msg=( - "Calling: \n" - "'func'(\n" - " # POSITIONAL_OR_KEYWORD:\n" - " 'param_string'={string},\n" - " 'param_dictionary'={dictionary},\n" - ")".format( - string=logwrap.pretty_repr( - string, - indent=8, no_indent_start=True), - dictionary=logwrap.pretty_repr( - dictionary, - indent=8, no_indent_start=True) - ) - ) - ), - mock.call.log( - level=logging.DEBUG, - msg="Done: 'func' with result:\n{}".format( - logwrap.pretty_repr(result)) - ), - ], - self.logger.mock_calls, + "DEBUG>Calling: \n" + "'func'(\n" + " # POSITIONAL_OR_KEYWORD:\n" + " 'param_string'=u'''string''',\n" + " 'param_dictionary'=\n" + " dict({\n" + " 'key': u'''dictionary''',\n" + " }),\n" + ")\n" + "DEBUG>Done: 'func' with result:\n" + "tuple((\n" + " u'''string''',\n" + " dict({\n" + " 'key': u'''dictionary''',\n" + " }),\n" + "))\n", + self.stream.getvalue(), ) def test_005_args_kwargs(self): @@ -221,32 +149,31 @@ def func(*args, **kwargs): self.assertEqual(result, (tuple(targs), tkwargs)) self.assertEqual( - [ - mock.call.log( - level=logging.DEBUG, - msg=( - "Calling: \n" - "'func'(\n" - " # VAR_POSITIONAL:\n" - " 'args'={args},\n" - " # VAR_KEYWORD:\n" - " 'kwargs'={kwargs},\n)".format( - args=logwrap.pretty_repr( - tuple(targs), - indent=8, no_indent_start=True), - kwargs=logwrap.pretty_repr( - tkwargs, - indent=8, no_indent_start=True) - ) - ) - ), - mock.call.log( - level=logging.DEBUG, - msg="Done: 'func' with result:\n{}".format( - logwrap.pretty_repr(result)) - ), - ], - self.logger.mock_calls, + "DEBUG>Calling: \n" + "'func'(\n" + " # VAR_POSITIONAL:\n" + " 'args'=\n" + " tuple((\n" + " u'''string1''',\n" + " u'''string2''',\n" + " )),\n" + " # VAR_KEYWORD:\n" + " 'kwargs'=\n" + " dict({\n" + " 'key': u'''tkwargs''',\n" + " }),\n" + ")\n" + "DEBUG>Done: 'func' with result:\n" + "tuple((\n" + " tuple((\n" + " u'''string1''',\n" + " u'''string2''',\n" + " )),\n" + " dict({\n" + " 'key': u'''tkwargs''',\n" + " }),\n" + "))\n", + self.stream.getvalue(), ) def test_006_renamed_args_kwargs(self): @@ -262,37 +189,34 @@ def func(arg, *positional, **named): result = func(arg, *targs, **tkwargs) self.assertEqual(result, (arg, tuple(targs), tkwargs)) self.assertEqual( - [ - mock.call.log( - level=logging.DEBUG, - msg=( - "Calling: \n" - "'func'(\n" - " # POSITIONAL_OR_KEYWORD:\n" - " 'arg'={arg},\n" - " # VAR_POSITIONAL:\n" - " 'positional'={args},\n" - " # VAR_KEYWORD:\n" - " 'named'={kwargs},\n)".format( - arg=logwrap.pretty_repr( - arg, - indent=8, no_indent_start=True), - args=logwrap.pretty_repr( - tuple(targs), - indent=8, no_indent_start=True), - kwargs=logwrap.pretty_repr( - tkwargs, - indent=8, no_indent_start=True) - ) - ) - ), - mock.call.log( - level=logging.DEBUG, - msg="Done: 'func' with result:\n{}".format( - logwrap.pretty_repr(result)) - ), - ], - self.logger.mock_calls, + "DEBUG>Calling: \n" + "'func'(\n" + " # POSITIONAL_OR_KEYWORD:\n" + " 'arg'=u'''arg''',\n" + " # VAR_POSITIONAL:\n" + " 'positional'=\n" + " tuple((\n" + " u'''string1''',\n" + " u'''string2''',\n" + " )),\n" + " # VAR_KEYWORD:\n" + " 'named'=\n" + " dict({\n" + " 'key': u'''tkwargs''',\n" + " }),\n" + ")\n" + "DEBUG>Done: 'func' with result:\n" + "tuple((\n" + " u'''arg''',\n" + " tuple((\n" + " u'''string1''',\n" + " u'''string2''',\n" + " )),\n" + " dict({\n" + " 'key': u'''tkwargs''',\n" + " }),\n" + "))\n", + self.stream.getvalue(), ) def test_007_negative(self): @@ -304,18 +228,12 @@ def func(): func() self.assertEqual( - [ - mock.call.log( - level=logging.DEBUG, - msg="Calling: \n'func'()" - ), - mock.call.log( - level=logging.ERROR, - msg="Failed: \n'func'()", - exc_info=True - ), - ], - self.logger.mock_calls, + 'DEBUG>Calling: \n' + "'func'()\n" + 'ERROR>Failed: \n' + "'func'()\n" + 'Traceback (most recent call last):', + '\n'.join(self.stream.getvalue().split('\n')[:5]), ) def test_008_negative_substitutions(self): @@ -334,7 +252,6 @@ def func(): with self.assertRaises(ValueError): func() - self.assertEqual(len(self.logger.mock_calls), 0) self.assertEqual( [ mock.call( @@ -431,22 +348,14 @@ def __repr__(tst_self): result = tst.func() self.assertEqual(result, 'No args') self.assertEqual( - [ - mock.call.log( - level=logging.DEBUG, - msg="Calling: \n" - "'func'(\n" - " # POSITIONAL_OR_KEYWORD:\n" - " 'tst_self'=,\n" - ")" - ), - mock.call.log( - level=logging.DEBUG, - msg="Done: 'func' with result:\n{}".format( - logwrap.pretty_repr(result)) - ), - ], - self.logger.mock_calls, + "DEBUG>Calling: \n" + "'func'(\n" + " # POSITIONAL_OR_KEYWORD:\n" + " 'tst_self'=,\n" + ")\n" + "DEBUG>Done: 'func' with result:\n" + "u'''No args'''\n", + self.stream.getvalue(), ) def test_012_class_decorator(self): @@ -457,24 +366,13 @@ def func(): result = func() self.assertEqual(result, 'No args') self.assertEqual( - [ - mock.call.log( - level=logging.DEBUG, - msg="Calling: \n'func'()" - ), - mock.call.log( - level=logging.DEBUG, - msg="Done: 'func' with result:\n{}".format( - logwrap.pretty_repr(result)) - ), - ], - self.logger.mock_calls, + "DEBUG>Calling: \n" + "'func'()\n" + "DEBUG>Done: 'func' with result:\n" + "u'''No args'''\n", + self.stream.getvalue(), ) - @unittest.skipUnless( - six.PY3, - 'Strict python 3 syntax' - ) def test_013_py3_args(self): new_logger = mock.Mock(spec=logging.Logger, name='logger') log = mock.Mock(name='log') @@ -482,15 +380,10 @@ def test_013_py3_args(self): log_call = logwrap.logwrap(log=new_logger) - namespace = {} + def tst(arg, darg=1, *args, kwarg, dkwarg=4, **kwargs): + pass - exec(""" -def tst(arg, darg=1, *args, kwarg, dkwarg=4, **kwargs): - pass - """, - namespace - ) - wrapped = log_call(namespace['tst']) + wrapped = log_call(tst) wrapped(0, 1, 2, kwarg=3, somekwarg=5) self.assertEqual( @@ -527,7 +420,7 @@ def tst(arg, darg=1, *args, kwarg, dkwarg=4, **kwargs): def test_014_wrapped(self): # noinspection PyShadowingNames def simpledeco(func): - @six.wraps(func) + @functools.wraps(func) def wrapped(*args, **kwargs): return func(*args, **kwargs) return wrapped @@ -543,40 +436,34 @@ def func(arg, darg=1, *args, **kwargs): (0, 1, (2, ), {'arg3': 3}) ) self.assertEqual( - [ - mock.call.log( - level=10, - msg="Calling: \n" - "'func'(\n" - " # POSITIONAL_OR_KEYWORD:\n" - " 'arg'=0,\n" - " 'darg'=1,\n" - " # VAR_POSITIONAL:\n" - " 'args'=\n" - " tuple((\n" - " 2,\n" - " )),\n" - " # VAR_KEYWORD:\n" - " 'kwargs'=\n" - " dict({\n" - " 'arg3': 3,\n" - " }),\n" - ")"), - mock.call.log( - level=10, - msg="Done: 'func' with result:\n" - "tuple((\n" - " 0,\n" - " 1,\n" - " tuple((\n" - " 2,\n" - " )),\n" - " dict({\n" - " 'arg3': 3,\n" - " }),\n" - "))") - ], - self.logger.mock_calls, + "DEBUG>Calling: \n" + "'func'(\n" + " # POSITIONAL_OR_KEYWORD:\n" + " 'arg'=0,\n" + " 'darg'=1,\n" + " # VAR_POSITIONAL:\n" + " 'args'=\n" + " tuple((\n" + " 2,\n" + " )),\n" + " # VAR_KEYWORD:\n" + " 'kwargs'=\n" + " dict({\n" + " 'arg3': 3,\n" + " }),\n" + ")\n" + "DEBUG>Done: 'func' with result:\n" + "tuple((\n" + " 0,\n" + " 1,\n" + " tuple((\n" + " 2,\n" + " )),\n" + " dict({\n" + " 'arg3': 3,\n" + " }),\n" + "))\n", + self.stream.getvalue(), ) def test_015_args_blacklist(self): @@ -632,7 +519,6 @@ def func(): with self.assertRaises(TypeError): func() - self.assertEqual(len(self.logger.mock_calls), 0) self.assertEqual( [ mock.call( @@ -688,7 +574,6 @@ def func(test_arg1, test_arg2): with self.assertRaises(TypeError): func(arg1, arg2) - self.assertEqual(len(self.logger.mock_calls), 0) self.assertEqual( [ mock.call( @@ -741,7 +626,6 @@ def func(test_arg1, test_arg2): with self.assertRaises(TypeError): func(arg1, arg2) - self.assertEqual(len(self.logger.mock_calls), 0) self.assertEqual( [ mock.call( @@ -769,7 +653,6 @@ def func(): func() - self.assertEqual(len(self.logger.mock_calls), 0) self.assertEqual( [ mock.call( @@ -792,24 +675,16 @@ def func(*args, **kwargs): result = func() self.assertEqual(result, 'No args') self.assertEqual( - [ - mock.call.log( - level=logging.DEBUG, - msg="Calling: \n" - "'func'(\n" - " # VAR_POSITIONAL:\n" - " 'args'=(),\n" - " # VAR_KEYWORD:\n" - " 'kwargs'={},\n" - ")" - ), - mock.call.log( - level=logging.DEBUG, - msg="Done: 'func' with result:\n{}".format( - logwrap.pretty_repr(result)) - ), - ], - self.logger.mock_calls, + "DEBUG>Calling: \n" + "'func'(\n" + " # VAR_POSITIONAL:\n" + " 'args'=(),\n" + " # VAR_KEYWORD:\n" + " 'kwargs'={},\n" + ")\n" + "DEBUG>Done: 'func' with result:\n" + "u'''No args'''\n", + self.stream.getvalue(), ) diff --git a/test/test_log_wrap_py3.py b/test/test_log_wrap_py3.py index e7c6728..0387926 100644 --- a/test/test_log_wrap_py3.py +++ b/test/test_log_wrap_py3.py @@ -16,28 +16,17 @@ """Python 3 specific tests""" -try: - import asyncio -except ImportError: - asyncio = None +import asyncio +import io import logging -import sys import typing # noqa # pylint: disable=unused-import import unittest -try: - from unittest import mock -except ImportError: - # noinspection PyUnresolvedReferences - import mock +from unittest import mock import logwrap # noinspection PyUnusedLocal,PyMissingOrEmptyDocstring -@unittest.skipIf( - asyncio is None, - 'Strict python 3.3+ API' -) class TestLogWrapAsync(unittest.TestCase): @classmethod def setUpClass(cls): @@ -48,15 +37,19 @@ def setUp(self): Due to no possibility of proper mock patch of function defaults, modify directly. """ - self.logger = mock.Mock(spec=logging.Logger) - self.logwrap_defaults = logwrap.logwrap.__kwdefaults__['log'] - self.logwrap_cls_defaults = logwrap.LogWrap.__init__.__kwdefaults__['log'] - logwrap.logwrap.__kwdefaults__['log'] = logwrap.LogWrap.__init__.__kwdefaults__['log'] = self.logger + self.logger = logging.getLogger('logwrap') + self.logger.setLevel(logging.DEBUG) + + self.stream = io.StringIO() + + self.logger.handlers.clear() + handler = logging.StreamHandler(self.stream) + handler.setFormatter(logging.Formatter(fmt='%(levelname)s>%(message)s')) + self.logger.addHandler(handler) def tearDown(self): """Revert modifications.""" - logwrap.LogWrap.__init__.__kwdefaults__['log'] = self.logwrap_cls_defaults - logwrap.logwrap.__kwdefaults__['log'] = self.logwrap_defaults + self.logger.handlers.clear() def test_coroutine_async(self): @logwrap.logwrap @@ -66,17 +59,11 @@ def func(): self.loop.run_until_complete(func()) self.assertEqual( - [ - mock.call.log( - level=logging.DEBUG, - msg="Awaiting: \n'func'()" - ), - mock.call.log( - level=logging.DEBUG, - msg="Done: 'func' with result:\nNone" - ) - ], - self.logger.mock_calls, + "DEBUG>Awaiting: \n" + "'func'()\n" + "DEBUG>Done: 'func' with result:\n" + "None\n", + self.stream.getvalue(), ) def test_coroutine_async_as_argumented(self): @@ -115,18 +102,12 @@ def func(): self.loop.run_until_complete(func()) self.assertEqual( - [ - mock.call.log( - level=logging.DEBUG, - msg="Awaiting: \n'func'()" - ), - mock.call.log( - level=logging.ERROR, - msg="Failed: \n'func'()", - exc_info=True - ) - ], - self.logger.mock_calls, + 'DEBUG>Awaiting: \n' + "'func'()\n" + 'ERROR>Failed: \n' + "'func'()\n" + 'Traceback (most recent call last):', + '\n'.join(self.stream.getvalue().split('\n')[:5]), ) def test_exceptions_blacklist(self): @@ -145,7 +126,6 @@ def func(): # While we're not expanding result coroutine object from namespace, # do not check execution result - self.assertEqual(len(self.logger.mock_calls), 0) self.assertEqual( [ mock.call( @@ -158,53 +138,39 @@ def func(): # noinspection PyUnusedLocal,PyMissingOrEmptyDocstring -@unittest.skipIf( - sys.version_info[:2] < (3, 4), - 'Strict python 3.3+ API' -) class TestAnnotated(unittest.TestCase): def setUp(self): """Preparation for tests. Due to no possibility of proper mock patch of function defaults, modify directly. """ - self.logger = mock.Mock(spec=logging.Logger) - self.logwrap_defaults = logwrap.logwrap.__kwdefaults__['log'] - self.logwrap_cls_defaults = logwrap.LogWrap.__init__.__kwdefaults__['log'] - logwrap.logwrap.__kwdefaults__['log'] = logwrap.LogWrap.__init__.__kwdefaults__['log'] = self.logger + self.logger = logging.getLogger('logwrap') + self.logger.setLevel(logging.DEBUG) + + self.stream = io.StringIO() + + self.logger.handlers.clear() + handler = logging.StreamHandler(self.stream) + handler.setFormatter(logging.Formatter(fmt='%(levelname)s>%(message)s')) + self.logger.addHandler(handler) def tearDown(self): """Revert modifications.""" - logwrap.LogWrap.__init__.__kwdefaults__['log'] = self.logwrap_cls_defaults - logwrap.logwrap.__kwdefaults__['log'] = self.logwrap_defaults + self.logger.handlers.clear() def test_annotation_args(self): - namespace = {'logwrap': logwrap} - - exec(""" -import typing -@logwrap.logwrap -def func(a: typing.Optional[int]=None): - pass - """, - namespace - ) - func = namespace['func'] # type: typing.Callable[..., None] + @logwrap.logwrap + def func(a: typing.Optional[int] = None): + pass + func() self.assertEqual( - [ - mock.call.log( - level=logging.DEBUG, - msg="Calling: \n" - "'func'(\n" - " # POSITIONAL_OR_KEYWORD:\n" - " 'a'=None, # type: typing.Union[int, NoneType]\n" - ")" - ), - mock.call.log( - level=logging.DEBUG, - msg="Done: 'func' with result:\nNone" - ) - ], - self.logger.mock_calls, + "DEBUG>Calling: \n" + "'func'(\n" + " # POSITIONAL_OR_KEYWORD:\n" + " 'a'=None, # type: typing.Union[int, NoneType]\n" + ")\n" + "DEBUG>Done: 'func' with result:\n" + "None\n", + self.stream.getvalue(), ) diff --git a/test/test_log_wrap_py35.py b/test/test_log_wrap_py35.py index 7498e8b..edd4131 100644 --- a/test/test_log_wrap_py35.py +++ b/test/test_log_wrap_py35.py @@ -16,27 +16,16 @@ """Python 3 specific tests""" -try: - import asyncio -except ImportError: - asyncio = None +import asyncio +import io import logging -import sys import unittest -try: - from unittest import mock -except ImportError: - # noinspection PyUnresolvedReferences - import mock +from unittest import mock import logwrap # noinspection PyUnusedLocal,PyMissingOrEmptyDocstring -@unittest.skipIf( - asyncio is None or sys.version_info[:2] < (3, 5), - 'Strict python 3.5+ API' -) class TestLogWrapAsync(unittest.TestCase): """async def differs from asyncio.coroutine.""" @classmethod @@ -48,41 +37,32 @@ def setUp(self): Due to no possibility of proper mock patch of function defaults, modify directly. """ - self.logger = mock.Mock(spec=logging.Logger) - self.logwrap_defaults = logwrap.logwrap.__kwdefaults__['log'] - self.logwrap_cls_defaults = logwrap.LogWrap.__init__.__kwdefaults__['log'] - logwrap.logwrap.__kwdefaults__['log'] = logwrap.LogWrap.__init__.__kwdefaults__['log'] = self.logger + self.logger = logging.getLogger('logwrap') + self.logger.setLevel(logging.DEBUG) + + self.stream = io.StringIO() + + self.logger.handlers.clear() + handler = logging.StreamHandler(self.stream) + handler.setFormatter(logging.Formatter(fmt='%(levelname)s>%(message)s')) + self.logger.addHandler(handler) def tearDown(self): """Revert modifications.""" - logwrap.LogWrap.__init__.__kwdefaults__['log'] = self.logwrap_cls_defaults - logwrap.logwrap.__kwdefaults__['log'] = self.logwrap_defaults + self.logger.handlers.clear() def test_coroutine_async(self): - namespace = {'logwrap': logwrap} - - exec(""" -@logwrap.logwrap -async def func(): - pass - """, - namespace - ) - func = namespace['func'] + @logwrap.logwrap + async def func(): + pass self.loop.run_until_complete(func()) self.assertEqual( - [ - mock.call.log( - level=logging.DEBUG, - msg="Awaiting: \n'func'()" - ), - mock.call.log( - level=logging.DEBUG, - msg="Done: 'func' with result:\nNone" - ) - ], - self.logger.mock_calls, + "DEBUG>Awaiting: \n" + "'func'()\n" + "DEBUG>Done: 'func' with result:\n" + "None\n", + self.stream.getvalue(), ) def test_coroutine_async_as_argumented(self): @@ -90,16 +70,9 @@ def test_coroutine_async_as_argumented(self): log = mock.Mock(name='log') new_logger.attach_mock(log, 'log') - namespace = {'logwrap': logwrap, 'new_logger': new_logger} - - exec(""" -@logwrap.logwrap(log=new_logger) -async def func(): - pass - """, - namespace - ) - func = namespace['func'] + @logwrap.logwrap(log=new_logger) + async def func(): + pass self.loop.run_until_complete(func()) @@ -118,33 +91,20 @@ async def func(): ) def test_coroutine_fail(self): - namespace = {'logwrap': logwrap} - - exec(""" -@logwrap.logwrap -async def func(): - raise Exception('Expected') - """, - namespace - ) - func = namespace['func'] + @logwrap.logwrap + async def func(): + raise Exception('Expected') with self.assertRaises(Exception): self.loop.run_until_complete(func()) self.assertEqual( - [ - mock.call.log( - level=logging.DEBUG, - msg="Awaiting: \n'func'()" - ), - mock.call.log( - level=logging.ERROR, - msg="Failed: \n'func'()", - exc_info=True - ) - ], - self.logger.mock_calls, + 'DEBUG>Awaiting: \n' + "'func'()\n" + 'ERROR>Failed: \n' + "'func'()\n" + 'Traceback (most recent call last):', + '\n'.join(self.stream.getvalue().split('\n')[:5]), ) def test_exceptions_blacklist(self): @@ -152,16 +112,9 @@ def test_exceptions_blacklist(self): log = mock.Mock(name='log') new_logger.attach_mock(log, 'log') - namespace = {'logwrap': logwrap, 'new_logger': new_logger} - - exec(""" -@logwrap.logwrap(log=new_logger, blacklisted_exceptions=[TypeError]) -async def func(): - raise TypeError('Blacklisted') - """, - namespace - ) - func = namespace['func'] + @logwrap.logwrap(log=new_logger, blacklisted_exceptions=[TypeError]) + async def func(): + raise TypeError('Blacklisted') with self.assertRaises(TypeError): self.loop.run_until_complete(func()) @@ -169,7 +122,6 @@ async def func(): # While we're not expanding result coroutine object from namespace, # do not check execution result - self.assertEqual(len(self.logger.mock_calls), 0) self.assertEqual( [ mock.call( diff --git a/test/test_log_wrap_shared.py b/test/test_log_wrap_shared.py index b6caf2d..98a3833 100644 --- a/test/test_log_wrap_shared.py +++ b/test/test_log_wrap_shared.py @@ -16,24 +16,11 @@ """_repr_utils (internal helpers) specific tests.""" -from __future__ import absolute_import -from __future__ import unicode_literals - -import sys +from inspect import signature import unittest -import six - # noinspection PyProtectedMember -from logwrap import _log_wrap_shared - -# pylint: disable=ungrouped-imports, no-name-in-module -if six.PY3: # pragma: no cover - from inspect import signature -else: # pragma: no cover - # noinspection PyUnresolvedReferences - from funcsigs import signature -# pylint: enable=ungrouped-imports, no-name-in-module +from logwrap import _log_wrap def example_function( @@ -48,7 +35,7 @@ def example_function( # noinspection PyUnusedLocal,PyMissingOrEmptyDocstring class TestBind(unittest.TestCase): def test_001_positive(self): - params = list(_log_wrap_shared.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) @@ -90,7 +77,7 @@ def test_001_positive(self): self.assertEqual(str(kwargs_bound), "**kwargs={}") def test_002_args_kwargs(self): - params = list(_log_wrap_shared.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') @@ -109,19 +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_shared.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_shared.BoundParameter(arg1_parameter, arg1_parameter.empty) + _log_wrap.BoundParameter(arg1_parameter, arg1_parameter.empty) - @unittest.skipIf(sys.version_info[:2] < (3, 4), 'python 3 syntax') def test_004_annotations(self): namespace = {} exec("""def func(arg1, arg2: int, arg3: int=3): pass""", namespace) func = namespace['func'] sig = signature(func) - params = list(_log_wrap_shared.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_pretty_str.py b/test/test_pretty_str.py index 3898bcb..8b8feff 100644 --- a/test/test_pretty_str.py +++ b/test/test_pretty_str.py @@ -18,9 +18,6 @@ """pretty_str specific tests""" -from __future__ import absolute_import -from __future__ import unicode_literals - import unittest import logwrap @@ -212,6 +209,3 @@ def __pretty_str__( result, "Test Class".format(id(Tst)) ) - - def test_py2_compatibility_flag(self): - self.assertIsInstance(logwrap.pretty_str(u'Text', py2_str=True), str) diff --git a/test/test_repr_utils.py b/test/test_repr_utils.py index 2e45147..46f64a4 100644 --- a/test/test_repr_utils.py +++ b/test/test_repr_utils.py @@ -18,10 +18,6 @@ """_repr_utils (internal helpers) specific tests.""" -from __future__ import absolute_import -from __future__ import unicode_literals - -import sys import unittest import logwrap @@ -284,15 +280,8 @@ def __pretty_repr__( "u''''''".format(id(Tst)) ) - def test_py2_compatibility_flag(self): - self.assertIsInstance(logwrap.pretty_repr(u'Text', py2_str=True), str) - # noinspection PyUnusedLocal,PyMissingOrEmptyDocstring -@unittest.skipIf( - sys.version_info[:2] < (3, 4), - 'Strict python 3.3+ API' -) class TestAnnotated(unittest.TestCase): def test_001_annotation_args(self): fmt = "\n{spc:<{indent}}<{obj!r} with interface ({args}){annotation}>".format diff --git a/tools/build-wheels.sh b/tools/build-wheels.sh index 032245f..0bc72ec 100755 --- a/tools/build-wheels.sh +++ b/tools/build-wheels.sh @@ -1,5 +1,5 @@ #!/bin/bash -PYTHON_VERSIONS="cp34-cp34m cp35-cp35m cp36-cp36m cp37-cp37m" +PYTHON_VERSIONS="cp35-cp35m cp36-cp36m cp37-cp37m" # Avoid creation of __pycache__/*.py[c|o] export PYTHONDONTWRITEBYTECODE=1 diff --git a/tox.ini b/tox.ini index 912d211..56551d5 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ [tox] minversion = 2.0 -envlist = pep8, pep257, py{27,34,35,36,37,py,py3}, pylint, docs, bandit, py{34,35,36}-nocov, mypy +envlist = pep8, pylint, mypy, bandit, pep257, py{35,36,37,py3}, docs, py{35,36,37}-nocov skipsdist = True skip_missing_interpreters = True @@ -20,29 +20,28 @@ deps = pytest-cov pytest-html pytest-sugar - py{34,35,36}-nocov: Cython + py{35,36,37}-nocov: Cython -r{toxinidir}/CI_REQUIREMENTS.txt - py{27,py}: mock commands = py.test -vv --junitxml=unit_result.xml --html=report.html --cov-config .coveragerc --cov-report html --cov=logwrap {posargs:test} coverage report --fail-under 87 -[testenv:py34-nocov] +[testenv:py35-nocov] usedevelop = False commands = python setup.py bdist_wheel pip install logwrap --no-index -f dist py.test -vv {posargs:test} -[testenv:py35-nocov] +[testenv:py36-nocov] usedevelop = False commands = python setup.py bdist_wheel pip install logwrap --no-index -f dist py.test -vv {posargs:test} -[testenv:py36-nocov] +[testenv:py37-nocov] usedevelop = False commands = python setup.py bdist_wheel @@ -53,12 +52,9 @@ commands = commands = {posargs:} [tox:travis] -2.7 = install, py27, -3.4 = py34, 3.5 = py35, 3.6 = py36, -3.7 = install, py37, -pypy = install, pypy, +3.7 = py37, pypy3 = install, pypy3, [testenv:pep8] @@ -80,7 +76,7 @@ commands = pip install ./ -vvv -U [testenv:pylint] deps = - pylint<2 + pylint >= 2.0 -r{toxinidir}/CI_REQUIREMENTS.txt commands = pylint logwrap