diff --git a/.coveragerc b/.coveragerc index 179c5cd..743ba49 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,4 +2,14 @@ source = logwrap omit = - test/* \ No newline at end of file + test/* +[report] +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + + # Don't complain about missing debug-only code: + def __repr__ + + # Don't complain if non-runnable code isn't run: + if __name__ == .__main__.: diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 39f201d..5365948 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,8 +4,9 @@ Version 3.3.0 ------------- * Type hints and stubs * PEP0518 -* Deprecation of *args for logwrap -* Fix empty *args and **kwargs +* Deprecation of `*args` for logwrap +* Fix empty `*args` and `**kwargs` +* allow override for arguments repr processing (pre- and post-processing) Version 3.2.0 ------------- diff --git a/README.rst b/README.rst index f92271a..699672f 100644 --- a/README.rst +++ b/README.rst @@ -187,6 +187,11 @@ Example construction and read from test: On object change, variable types is validated. +In special cases, when special processing required for parameters logging (hide or change parameters in log), +it can be done by override `pre_process_param` and `post_process_param`. + +See API documentation for details. + pretty_repr ----------- diff --git a/doc/source/logwrap.rst b/doc/source/logwrap.rst index 4e7cf7e..bebe72e 100644 --- a/doc/source/logwrap.rst +++ b/doc/source/logwrap.rst @@ -47,7 +47,35 @@ API: Decorators: `LogWrap` class and `logwrap` function. :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:: 3.3.0 Deprecation of `*args` + + .. py:method:: pre_process_param(self, arg) + + Process parameter for the future logging. + + :param arg: bound parameter + :type arg: BoundParameter + :return: value, value override for logging or None if argument should not be logged. + :rtype: typing.Union[BoundParameter, typing.Tuple[BoundParameter, typing.Any], None] + + Override this method if some modifications required for parameter value before logging + + .. versionadded:: 3.3.0 + + .. py:method:: post_process_param(self, arg, arg_repr) + + Process parameter for the future logging. + + :param arg: bound parameter + :type arg: BoundParameter + :param arg_repr: repr for value + :type arg_repr: six.text_type + :return: processed repr for value + :rtype: six.text_type + + Override this method if some modifications required for result of repr() over parameter + + .. versionadded:: 3.3.0 .. note:: Attributes/properties names the same as argument names and changes the same fields. @@ -76,3 +104,104 @@ API: Decorators: `LogWrap` class and `logwrap` function. :returns: Decorated function. On python 3.3+ awaitable is supported. :rtype: typing.Union[typing.Callable, typing.Awaitable] + + +.. py:class:: BoundParameter(object) + + Parameter-like object store BOUND with value parameter. + + .. versionadded:: 3.3.0 + + .. py:method:: __init__(self, parameter, value=Parameter.empty) + + Parameter-like object store BOUND with value parameter. + + :param parameter: parameter from signature + :type parameter: ``inspect.Parameter`` + :param value: parameter real value + :type value: typing.Any + :raises ValueError: No default value and no value + + .. py:attribute:: POSITIONAL_ONLY + + ``enum.IntEnum`` + Parameter.POSITIONAL_ONLY + + .. py:attribute:: POSITIONAL_OR_KEYWORD + + ``enum.IntEnum`` + Parameter.POSITIONAL_OR_KEYWORD + + .. py:attribute:: VAR_POSITIONAL + + ``enum.IntEnum`` + Parameter.VAR_POSITIONAL + + .. py:attribute:: KEYWORD_ONLY + + ``enum.IntEnum`` + Parameter.KEYWORD_ONLY + + .. py:attribute:: VAR_KEYWORD + + ``enum.IntEnum`` + Parameter.VAR_KEYWORD + + .. py:attribute:: empty + + ``typing.Type`` + Parameter.empty + + .. py:attribute:: parameter + + Parameter object. + + :rtype: inspect.Parameter + + .. py:attribute:: name + + Parameter name. + + :rtype: typing.Union[None, str] + + .. py:attribute:: default + + Parameter default value. + + :rtype: typing.Any + + .. py:attribute:: annotation + + Parameter annotation. + + :rtype: typing.Union[Parameter.empty, str] + + .. py:attribute:: kind + + Parameter kind. + + :rtype: enum.IntEnum + + .. py:attribute:: value + + Parameter value. + + :rtype: typing.Any + + .. py:method:: __hash__(self) + + Block hashing. + + :raises TypeError: Not hashable. + + +.. py:function:: bind_args_kwargs(sig, *args, **kwargs) + + Bind `*args` and `**kwargs` to signature and get Bound Parameters. + + :param sig: source signature + :type sig: inspect.Signature + :return: Iterator for bound parameters with all information about it + :rtype: typing.Iterator[BoundParameter] + + .. versionadded:: 3.3.0 diff --git a/logwrap/__init__.py b/logwrap/__init__.py index 77e21c1..c3a1e69 100644 --- a/logwrap/__init__.py +++ b/logwrap/__init__.py @@ -33,6 +33,7 @@ pretty_repr, pretty_str ) +from ._log_wrap_shared import BoundParameter, bind_args_kwargs PY3 = sys.version_info[:2] > (3, 0) # type: bool @@ -50,7 +51,9 @@ 'PrettyRepr', 'PrettyStr', 'pretty_repr', - 'pretty_str' + 'pretty_str', + 'BoundParameter', + 'bind_args_kwargs' ) __version__ = '3.2.2' diff --git a/logwrap/_log_wrap2.py b/logwrap/_log_wrap2.py index 9464dde..555c0f2 100644 --- a/logwrap/_log_wrap2.py +++ b/logwrap/_log_wrap2.py @@ -56,17 +56,10 @@ def old_spec( # pylint: enable=unused-argument sig = funcsigs.signature(old_spec) # type: funcsigs.Signature - parameters = tuple(sig.parameters.values()) # type: typing.Tuple[funcsigs.Parameter, ...] - - real_parameters = { - parameter.name: parameter.default for parameter in parameters - } # type: typing.Dict[str, typing.Any] - - bound = sig.bind(*args, **kwargs).arguments final_kwargs = { - key: bound.get(key, real_parameters[key]) - for key in real_parameters + parameter.name: parameter.value + for parameter in _log_wrap_shared.bind_args_kwargs(sig, *args, **kwargs) } # type: typing.Dict[str, typing.Any] return final_kwargs diff --git a/logwrap/_log_wrap2.pyi b/logwrap/_log_wrap2.pyi index 2513215..027e34a 100644 --- a/logwrap/_log_wrap2.pyi +++ b/logwrap/_log_wrap2.pyi @@ -3,6 +3,9 @@ import typing from . import _log_wrap_shared class LogWrap(_log_wrap_shared.BaseLogWrap): + + __slots__ = () + def __init__( self, func: typing.Optional[typing.Callable]=None, diff --git a/logwrap/_log_wrap3.py b/logwrap/_log_wrap3.py index c82f692..c808c5f 100644 --- a/logwrap/_log_wrap3.py +++ b/logwrap/_log_wrap3.py @@ -57,17 +57,10 @@ def old_spec( # pylint: enable=unused-argument sig = inspect.signature(old_spec) # type: inspect.Signature - parameters = tuple(sig.parameters.values()) # type: typing.Tuple[inspect.Parameter, ...] - - real_parameters = { - parameter.name: parameter.default for parameter in parameters - } # type: typing.Dict[str, typing.Any] - - bound = sig.bind(*args, **kwargs).arguments final_kwargs = { - key: bound.get(key, real_parameters[key]) - for key in real_parameters + parameter.name: parameter.value + for parameter in _log_wrap_shared.bind_args_kwargs(sig, *args, **kwargs) } # type: typing.Dict[str, typing.Any] return final_kwargs diff --git a/logwrap/_log_wrap3.pyi b/logwrap/_log_wrap3.pyi index 6d16152..7ea74fe 100644 --- a/logwrap/_log_wrap3.pyi +++ b/logwrap/_log_wrap3.pyi @@ -3,6 +3,9 @@ import typing from . import _log_wrap_shared class LogWrap(_log_wrap_shared.BaseLogWrap): + + __slots__ = () + def __init__( self, func: typing.Optional[typing.Callable]=None, diff --git a/logwrap/_log_wrap_shared.py b/logwrap/_log_wrap_shared.py index d886294..274454c 100644 --- a/logwrap/_log_wrap_shared.py +++ b/logwrap/_log_wrap_shared.py @@ -29,7 +29,21 @@ import logwrap as core from . import _class_decorator -__all__ = ('BaseLogWrap', ) +# 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') logger = logging.getLogger(__name__) # type: logging.Logger @@ -66,9 +80,146 @@ def wrapper(self, val): return wrapper return deco -# pylint: disable=assigning-non-slot,abstract-method +class BoundParameter(object): + """Parameter-like object store BOUND with value parameter. + + .. versionadded:: 3.3.0 + """ + + __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 = Parameter.empty + + def __init__( + self, + parameter, # type: Parameter + value=Parameter.empty # type: typing.Any + ): # type: (...) -> None + """Parameter-like object store BOUND with value parameter. + + :param parameter: parameter from signature + :type parameter: inspect.Parameter + :param value: parameter real value + :type value: typing.Any + :raises ValueError: No default value and no value + """ + self._parameter = parameter + + if value is self.empty: + if parameter.default is self.empty and parameter.kind not in (self.VAR_POSITIONAL, self.VAR_KEYWORD): + raise ValueError('Value is not set and no default value') + self._value = parameter.default + else: + self._value = value + + @property + def parameter(self): # type: () -> Parameter + """Parameter object.""" + return self._parameter + + @property + def name(self): # type: () -> typing.Union[None, str] + """Parameter name.""" + return self.parameter.name + + @property + def default(self): # type: () -> typing.Any + """Parameter default value.""" + return self.parameter.default + + @property + def annotation(self): # type: () -> typing.Union[Parameter.empty, str] + """Parameter annotation.""" + return self.parameter.annotation + + @property + def kind(self): # type: () -> int + """Parameter kind.""" + return self.parameter.kind + + @property + def value(self): # type: () -> typing.Any + """Parameter value.""" + return self._value + + def __hash__(self): # pragma: no cover + """Block hashing. + + :raises TypeError: Not hashable. + """ + msg = "unhashable type: '{0}'".format(self.__class__.__name__) + raise TypeError(msg) + + def __str__(self): + """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) + + # 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)) + + value = self.value + if self.empty == value: + if self.VAR_POSITIONAL == self.kind: + value = () + elif self.VAR_KEYWORD == self.kind: + value = {} + + as_str += '={value!r}'.format(value=value) + + if self.default is not self.empty: + as_str += ' # {self.default!r}'.format(self=self) + + if self.kind == self.VAR_POSITIONAL: + as_str = '*' + as_str + elif self.kind == self.VAR_KEYWORD: + as_str = '**' + as_str + + return as_str + + def __repr__(self): + """Debug purposes.""" + return '<{} "{}">'.format(self.__class__.__name__, self) + +def bind_args_kwargs( + sig, # type: Signature + *args, + **kwargs +): # type: (...) -> typing.Iterator[BoundParameter] + """Bind *args and **kwargs to signature and get Bound Parameters. + + :param sig: source signature + :type sig: Signature + :return: Iterator for bound parameters with all information about it + :rtype: typing.Iterator[BoundParameter] + + .. versionadded:: 3.3.0 + """ + bound = sig.bind(*args, **kwargs).arguments + parameters = list(sig.parameters.values()) + for param in parameters: + yield BoundParameter( + parameter=param, + value=bound.get(param.name, param.default) + ) + + +# pylint: disable=assigning-non-slot,abstract-method # noinspection PyAbstractClass class BaseLogWrap(_class_decorator.BaseDecorator): """Base class for LogWrap implementation.""" @@ -320,6 +471,62 @@ def __repr__(self): ) ) + @staticmethod + def _bind_args_kwargs( + sig, # type: Signature + *args, + **kwargs + ): # type: (...) -> typing.Iterator[BoundParameter] + """Bind *args and **kwargs to signature and get Bound Parameters. + + :param sig: source signature + :type sig: 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) + + # 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] + """Process parameter for the future logging. + + :param arg: bound parameter + :type arg: BoundParameter + :return: value, value override for logging or None if argument should not be logged. + :rtype: typing.Union[BoundParameter, typing.Tuple[BoundParameter, typing.Any], None] + + Override this method if some modifications required for parameter value before logging + + .. versionadded:: 3.3.0 + """ + return arg + + # noinspection PyMethodMayBeStatic,PyUnusedLocal + def post_process_param( # pylint: disable=no-self-use,unused-argument + self, + arg, # type: BoundParameter + arg_repr # type: six.text_type + ): # type: (...) -> six.text_type + """Process parameter for the future logging. + + :param arg: bound parameter + :type arg: BoundParameter + :param arg_repr: repr for value + :type arg_repr: six.text_type + :return: processed repr for value + :rtype: six.text_type + + Override this method if some modifications required for result of repr() over parameter + + .. versionadded:: 3.3.0 + """ + return arg_repr + def _get_func_args_repr( self, sig, # type: inspect.Signature @@ -332,38 +539,50 @@ def _get_func_args_repr( :type args: tuple :type kwargs: dict :rtype: str + + .. versionchanged:: 3.3.0 Use pre- and post- processing of params during execution """ if not (self.log_call_args or self.log_call_args_on_exc): return '' - bound = sig.bind(*args, **kwargs).arguments - param_str = "" last_kind = None - for param in sig.parameters.values(): + for param in self._bind_args_kwargs(sig, *args, **kwargs): if param.name in self.blacklisted_names: continue - if last_kind != param.kind: - param_str += comment(kind=param.kind) - last_kind = param.kind + preprocessed = self.pre_process_param(param) + if preprocessed is None: + continue + + if isinstance(preprocessed, (tuple, list)): + param, value = preprocessed + else: + value = param.value - src = bound.get(param.name, param.default) - if param.empty == src: + if param.empty == value: if param.VAR_POSITIONAL == param.kind: - src = () + value = () elif param.VAR_KEYWORD == param.kind: - src = {} + value = {} + + val = core.pretty_repr( + src=value, + indent=indent + 4, + no_indent_start=True, + max_indent=self.max_indent, + ) + + val = self.post_process_param(param, val) + + if last_kind != param.kind: + param_str += comment(kind=param.kind) + last_kind = param.kind param_str += fmt( key=param.name, - val=core.pretty_repr( - src=src, - indent=indent + 4, - no_indent_start=True, - max_indent=self.max_indent, - ), + val=val, ) if param_str: param_str += "\n" diff --git a/logwrap/_log_wrap_shared.pyi b/logwrap/_log_wrap_shared.pyi index 2231e54..88102d5 100644 --- a/logwrap/_log_wrap_shared.pyi +++ b/logwrap/_log_wrap_shared.pyi @@ -1,3 +1,4 @@ +import enum import inspect import logging import typing @@ -6,10 +7,66 @@ 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 # type: enum.IntEnum + POSITIONAL_OR_KEYWORD = Parameter.POSITIONAL_OR_KEYWORD # type: enum.IntEnum + VAR_POSITIONAL = Parameter.VAR_POSITIONAL # type: enum.IntEnum + KEYWORD_ONLY = Parameter.KEYWORD_ONLY # type: enum.IntEnum + VAR_KEYWORD = Parameter.VAR_KEYWORD # type: enum.IntEnum + + empty = Parameter.empty # type: typing.Type + + 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, + **kwargs + ) -> typing.Iterator[BoundParameter]: ... + + class BaseLogWrap(_class_decorator.BaseDecorator): def __init__( self, @@ -74,6 +131,24 @@ class BaseLogWrap(_class_decorator.BaseDecorator): @property def _spec(self) -> typing.Callable: ... + @staticmethod + def _bind_args_kwargs( + sig: Signature, + *args, + **kwargs + ) -> 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: six.text_type + ) -> six.text_type: ... + def _get_func_args_repr( self, sig: inspect.Signature, diff --git a/setup.py b/setup.py index 9f1d299..af8d701 100644 --- a/setup.py +++ b/setup.py @@ -258,6 +258,7 @@ def get_simple_vars_from_src(src): extras_require={ ':python_version == "2.7"': [ 'funcsigs>=1.0', + 'enum34>=1.1', ], }, install_requires=required, diff --git a/test/test_log_wrap.py b/test/test_log_wrap.py index 724c1a8..835eb6f 100644 --- a/test/test_log_wrap.py +++ b/test/test_log_wrap.py @@ -1,4 +1,4 @@ -# Copyright 2016 - 2017 Alexey Stepanov aka penguinolog +# Copyright 2016 - 2018 Alexey Stepanov aka penguinolog # Copyright 2016 Mirantis, Inc. @@ -750,7 +750,7 @@ def func(): class TestObject(unittest.TestCase): - def test_basic(self): + def test_001_basic(self): log_call = logwrap.LogWrap() self.assertEqual(log_call.log_level, logging.DEBUG) self.assertEqual(log_call.exc_level, logging.ERROR) @@ -802,6 +802,113 @@ def test_basic(self): repr(log_call), ) + def test_002_override_skip_arg(self): + class SkipArg(logwrap.LogWrap): + def pre_process_param( + self, + arg, + ): + if 'skip' in arg.name: + return None + return arg + + log = mock.Mock(spec=logging.Logger, name='logger') + + wrapper = SkipArg(log=log, log_result_obj=False) + + @wrapper + def func(arg, arg_skip, arg2=None, skip_arg=None): + pass + + func(1, 2) + self.assertEqual( + log.mock_calls, + [ + mock.call.log( + level=logging.DEBUG, + msg="Calling: \n" + "'func'(\n" + " # POSITIONAL_OR_KEYWORD:\n" + " 'arg'=1,\n" + " 'arg2'=None,\n" + ")"), + mock.call.log(level=logging.DEBUG, msg="Done: 'func'") + ] + ) + + def test_003_override_change_arg(self): + class ChangeArg(logwrap.LogWrap): + def pre_process_param( + self, + arg, + ): + if 'secret' in arg.name: + return arg, None + return arg + + log = mock.Mock(spec=logging.Logger, name='logger') + + wrapper = ChangeArg(log=log, log_result_obj=False) + + @wrapper + def func(arg, arg_secret, arg2='public', secret_arg=('key')): + pass + + func('data', 'key') + self.assertEqual( + log.mock_calls, + [ + mock.call.log( + level=logging.DEBUG, + msg="Calling: \n" + "'func'(\n" + " # POSITIONAL_OR_KEYWORD:\n" + " 'arg'=u'''data''',\n" + " 'arg_secret'=None,\n" + " 'arg2'=u'''public''',\n" + " 'secret_arg'=None,\n" + ")"), + mock.call.log(level=logging.DEBUG, msg="Done: 'func'") + ] + ) + + def test_003_override_change_repr(self): + class ChangeRepr(logwrap.LogWrap): + def post_process_param( + self, + arg, + arg_repr + ): + if 'secret' in arg.name: + return "<*hidden*>" + return arg_repr + + log = mock.Mock(spec=logging.Logger, name='logger') + + wrapper = ChangeRepr(log=log, log_result_obj=False) + + @wrapper + def func(arg, arg_secret, arg2='public', secret_arg=('key')): + pass + + func('data', 'key') + self.assertEqual( + log.mock_calls, + [ + mock.call.log( + level=logging.DEBUG, + msg="Calling: \n" + "'func'(\n" + " # POSITIONAL_OR_KEYWORD:\n" + " 'arg'=u'''data''',\n" + " 'arg_secret'=<*hidden*>,\n" + " 'arg2'=u'''public''',\n" + " 'secret_arg'=<*hidden*>,\n" + ")"), + mock.call.log(level=logging.DEBUG, msg="Done: 'func'") + ] + ) + # noinspection PyUnusedLocal,PyMissingOrEmptyDocstring @mock.patch('logwrap._log_wrap_shared.logger', autospec=True) diff --git a/test/test_log_wrap_py3.py b/test/test_log_wrap_py3.py index 6f8a485..be03200 100644 --- a/test/test_log_wrap_py3.py +++ b/test/test_log_wrap_py3.py @@ -1,6 +1,4 @@ -# Copyright 2016 - 2017 Alexey Stepanov aka penguinolog - -# Copyright 2016 Mirantis, Inc. +# Copyright 2016 - 2018 Alexey Stepanov aka penguinolog # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain diff --git a/test/test_log_wrap_py35.py b/test/test_log_wrap_py35.py index c200fc2..2f0c00f 100644 --- a/test/test_log_wrap_py35.py +++ b/test/test_log_wrap_py35.py @@ -1,6 +1,4 @@ -# Copyright 2016 - 2017 Alexey Stepanov aka penguinolog - -# Copyright 2016 Mirantis, Inc. +# Copyright 2016 - 2018 Alexey Stepanov aka penguinolog # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain diff --git a/test/test_log_wrap_shared.py b/test/test_log_wrap_shared.py new file mode 100644 index 0000000..b6caf2d --- /dev/null +++ b/test/test_log_wrap_shared.py @@ -0,0 +1,148 @@ +# Copyright 2018 Alexey Stepanov aka penguinolog + +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +# pylint: disable=missing-docstring + +"""_repr_utils (internal helpers) specific tests.""" + +from __future__ import absolute_import +from __future__ import unicode_literals + +import sys +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 + + +def example_function( + arg1, arg2=2, arg3=3, *args, **kwargs +): + pass + + +sig = signature(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)) + arg_1_bound = params[0] + self.assertEqual(arg_1_bound.name, 'arg1') + self.assertEqual(arg_1_bound.value, 1) + self.assertEqual(arg_1_bound.default, arg_1_bound.empty) + self.assertEqual(arg_1_bound.annotation, arg_1_bound.empty) + self.assertEqual(arg_1_bound.kind, arg_1_bound.POSITIONAL_OR_KEYWORD) + self.assertEqual(str(arg_1_bound), "arg1=1") + + arg_2_bound = params[1] + self.assertEqual(arg_2_bound.name, 'arg2') + self.assertEqual(arg_2_bound.value, 2) + self.assertEqual(arg_2_bound.default, 2) + self.assertEqual(arg_2_bound.annotation, arg_2_bound.empty) + self.assertEqual(arg_2_bound.kind, arg_2_bound.POSITIONAL_OR_KEYWORD) + self.assertEqual(str(arg_2_bound), "arg2=2 # 2") + + arg_3_bound = params[2] + self.assertEqual(arg_3_bound.name, 'arg3') + self.assertEqual(arg_3_bound.value, 33) + self.assertEqual(arg_3_bound.default, 3) + self.assertEqual(arg_3_bound.annotation, arg_3_bound.empty) + self.assertEqual(arg_3_bound.kind, arg_3_bound.POSITIONAL_OR_KEYWORD) + self.assertEqual(str(arg_3_bound), "arg3=33 # 3") + + args_bound = params[3] + self.assertEqual(args_bound.name, 'args') + self.assertEqual(args_bound.value, args_bound.empty) + self.assertEqual(args_bound.default, args_bound.empty) + self.assertEqual(args_bound.annotation, args_bound.empty) + self.assertEqual(args_bound.kind, args_bound.VAR_POSITIONAL) + self.assertEqual(str(args_bound), "*args=()") + + kwargs_bound = params[4] + self.assertEqual(kwargs_bound.name, 'kwargs') + self.assertEqual(kwargs_bound.value, kwargs_bound.empty) + self.assertEqual(kwargs_bound.default, kwargs_bound.empty) + self.assertEqual(kwargs_bound.annotation, kwargs_bound.empty) + self.assertEqual(kwargs_bound.kind, kwargs_bound.VAR_KEYWORD) + 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)) + + args_bound = params[3] + self.assertEqual(args_bound.name, 'args') + self.assertEqual(args_bound.value, (4,)) + self.assertEqual(args_bound.default, args_bound.empty) + self.assertEqual(args_bound.annotation, args_bound.empty) + self.assertEqual(args_bound.kind, args_bound.VAR_POSITIONAL) + self.assertEqual(str(args_bound), "*args=(4,)") + + kwargs_bound = params[4] + self.assertEqual(kwargs_bound.name, 'kwargs') + self.assertEqual(kwargs_bound.value, {'arg5': 5}) + self.assertEqual(kwargs_bound.default, kwargs_bound.empty) + self.assertEqual(kwargs_bound.annotation, kwargs_bound.empty) + self.assertEqual(kwargs_bound.kind, kwargs_bound.VAR_KEYWORD) + 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)) + arg_1_bound = params[0] + arg1_parameter = arg_1_bound.parameter + with self.assertRaises(ValueError): + _log_wrap_shared.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)) + + arg_1_bound = params[0] + self.assertEqual(arg_1_bound.name, 'arg1') + self.assertEqual(arg_1_bound.value, 1) + self.assertEqual(arg_1_bound.default, arg_1_bound.empty) + self.assertEqual(arg_1_bound.annotation, arg_1_bound.empty) + self.assertEqual(arg_1_bound.kind, arg_1_bound.POSITIONAL_OR_KEYWORD) + self.assertEqual(str(arg_1_bound), "arg1=1") + + arg_2_bound = params[1] + self.assertEqual(arg_2_bound.name, 'arg2') + self.assertEqual(arg_2_bound.value, 2) + self.assertEqual(arg_2_bound.default, arg_2_bound.empty) + self.assertEqual(arg_2_bound.annotation, int) + self.assertEqual(arg_2_bound.kind, arg_2_bound.POSITIONAL_OR_KEYWORD) + self.assertEqual(str(arg_2_bound), "arg2: int=2") + + arg_3_bound = params[2] + self.assertEqual(arg_3_bound.name, 'arg3') + self.assertEqual(arg_3_bound.value, 4) + self.assertEqual(arg_3_bound.default, 3) + self.assertEqual(arg_3_bound.annotation, int) + self.assertEqual(arg_3_bound.kind, arg_3_bound.POSITIONAL_OR_KEYWORD) + self.assertEqual(str(arg_3_bound), "arg3: int=4 # 3") diff --git a/tox.ini b/tox.ini index ec2fb9c..21f26c1 100644 --- a/tox.ini +++ b/tox.ini @@ -26,7 +26,7 @@ deps = 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 85 + coverage report --fail-under 87 [testenv:py34-nocov] usedevelop = False