<reponame>Nachtfeuer/concept-py
"""
   Decorator tools.

.. module:: decorator
    :platform: Unix, Windows
    :synopis: decorator tools.

.. moduleauthor:: <NAME> <<EMAIL>>


   =======
   License
   =======
   Copyright (c) 2015 <NAME>

   Permission is hereby granted, free of charge, to any person obtaining a copy of this
   software and associated documentation files (the "Software"), to deal in the Software
   without restriction, including without limitation the rights to use, copy, modify, merge,
   publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
   to whom the Software is furnished to do so, subject to the following conditions:

   The above copyright notice and this permission notice shall be included in all copies
   or substantial portions of the Software.

   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
   IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
   DAMAGES OR OTHER LIABILITY,
   WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
   OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
import sys
import json
import inspect

if sys.version.startswith("2."):
    from types import NoneType
else:
    NoneType = type(None)


class data(object):
    """
    Decorator to provide data to a functor or method.

    The default is to get a parameter with name 'values' of type 'list'
    in your final function or method. For consistency reason this parameter
    will ALWAYS be a list.

    You can change the name using the parameter 'key'.
    Instead of using decorator parameter 'values' you can use 'file' to load
    the content of a file. If the filename ends with .json then it is
    loaded as json file.

    With parameter single you can run test decorated method as many entries
    are contained in the list.

    >>> @data(values=[1, 2, 3, 4])
    ... def test1(values): print(values)
    >>> test1()
    [1, 2, 3, 4]
    >>> @data(key='some_data', values=[4, 3, 2, 1])
    ... def test2(some_data): print(some_data)
    >>> test2()
    [4, 3, 2, 1]
    >>> @data(key='some_data', values=[2, 1], single=True)
    ... def test3(some_data): print(some_data)
    >>> test3()
    [2]
    [1]
    """

    def __init__(self, **kwargs):
        """
        Check the decorator parameters and keep the data for the call of the decorator.

        - valid parameters are 'key', 'file' and 'values'
        - you either can use 'file' OR 'values'
        - A file will be simple read as it is or as JSON if the name ends with '.json'
        """
        for key in kwargs:
            assert key in ['key', 'file', 'values', 'single']

        self.key = str(kwargs['key']) if 'key' in kwargs else "values"
        assert isinstance(self.key, str)
        self.values = kwargs['values'] if 'values' in kwargs else []
        assert isinstance(self.values, list)
        self.single = bool(kwargs['single']) if 'single' in kwargs else False

        if 'file' in kwargs:
            assert len(self.values) == 0
            if kwargs['file'].endswith(".json"):
                self.values.append(json.loads(open(kwargs['file'], 'r').read()))
            else:
                self.values.append(open(kwargs['file'], 'r').read())

    def __call__(self, function):
        """
        Providing the decorator for the function/method.

        When calling the decorator the defined values will
        be passed to the original function.

        :returns: decorator.
        """
        def decorator(*args):
            """The decorator function."""
            if self.single:
                for value in self.values:
                    arguments = {self.key: [value]}
                    function(*args, **arguments)
            else:
                arguments = {self.key: self.values}
                return function(*args, **arguments)

        decorator.__name__ = function.__name__
        decorator.__doc__ = function.__doc__
        return decorator


class validate_test_responsibility_for(object):
    """
    Decorator to ensure to test all methods for a given class.

    a class decorator that throws an exception when the test class does not
    implement all tests for all methods of the testable class (unit).
    The next code gives you an example on how it is used and what does happen:

    >>> class Value:
    ...     def __init__(self, value):
    ...         self.value = value
    ...
    >>> try:
    ...     import unittest
    ...     @validate_test_responsibility_for(Value)
    ...     class TestValue(unittest.TestCase):
    ...         pass
    ... except Exception as e:
    ...     print("|%s|" % str(e).strip())
    |...failed to provide test method 'TestValue.test_init' for method 'Value.__init__'|
    """

    def __init__(self, testable_class, include_class_name=False):
        """Store the class for test and checks all methods of that class."""
        if hasattr(testable_class, "decorated_object"):
            testable_class = testable_class.decorated_object

        self.testable_class = testable_class
        self.include_class_name = include_class_name
        self.methods_in_testable_class\
            = self.get_entries(self.testable_class, inspect.isfunction)\
            + self.get_entries(self.testable_class, inspect.ismethod)

    @staticmethod
    def get_entries(the_class, mode):
        """
        Query functions and method of a given class.

        get all entries by given mode (function or method) but the
        members of the concrete class only; not from its base
        """
        classes = {}
        for concrete_class in reversed(inspect.getmro(the_class)):
            classes[concrete_class] = {}
            for name, definition in dict(inspect.getmembers(concrete_class, mode)).items():
                if name.find("subclass") >= 0:
                    continue

                object_name = name
                is_base_method_only = False

                for known_class in classes:
                    if object_name not in classes[known_class]:
                        continue

                    if classes[known_class][object_name] == definition:
                        is_base_method_only = True
                        break

                if not is_base_method_only:
                    classes[concrete_class][object_name] = definition

        return list(classes[the_class].keys())

    def __call__(self, test_class):
        """Xalled when instantiated; then we have to verify for the required test methods."""
        self.verify(test_class)
        return test_class

    @staticmethod
    def get_test_method(name, prefix=""):
        """Adjusting final test method name."""
        # no underscores wanted (change "__init__" => "init")
        final_name = name.strip("_").lower()

        # ensure more readable name (like "equal" instead of "eq")
        if name == "__eq__":
            final_name = "equal"
        elif name == "__lt__":
            final_name = "less"
        elif name == "__gt__":
            final_name = "greater"

        return "test_" + prefix.lower() + final_name

    def verify(self, test_class):
        """Verification that for each testable method a test method does exist."""
        methods_in_test_class\
            = self.get_entries(test_class, inspect.isfunction)\
            + self.get_entries(test_class, inspect.ismethod)

        missing = []
        for testable_method in self.methods_in_testable_class:
            prefix = ""
            if self.include_class_name:
                prefix = self.testable_class.__name__ + "_"

            test_method = self.get_test_method(testable_method, prefix)
            if test_method in methods_in_test_class:
                continue

            missing.append((test_class.__name__ + "." + test_method,
                            self.testable_class.__name__ + "." + testable_method))

        if len(missing) > 0:
            # creates message with all missing methods throwing an exception for it
            message = ""
            for test_method, testable_method in missing:
                message += "\n...failed to provide test method '%s' for method '%s'" \
                           % (test_method, testable_method)
            raise Exception(message)


class validate_types(object):
    """
    A function or method decorator that validates types of parameters.

    >>> try:
    ...     @validate_types(ptypes=[int, int], r=int)
    ...     def sum_of_two_integers(a, b):
    ...         return a + b
    ...     sum_of_two_integers(1, "world")
    ... except TypeError as e:
    ...     print("|%s|" % str(e).replace('<class', '<type'))
    |1. parameter - expected type is <type 'int'>, given type is <type 'str'>|
    """

    def __init__(self, ptypes=[], r=NoneType, offset=0):
        """
        Register assertion information.

        :param: ptypes: list of parameter types like "int", "float" or "str"
        :param: e: return type
        :param: offset: number of parameters to ignore (from the beginning
        """
        self.parameter_types = ptypes
        self.return_type = r
        self.offset = offset

    def __call__(self, function):
        """
        Validating types.

        :param: function: that's the real function which should be decorated
        :rtype: decorator function (wrapper)
        """
        def wrapper(*args, **kwargs):
            """
            The wrapper checks the types of the parameter of the real function.

            It raises an exception if the type doesn't fit. If fine the
            real function is executed and the return value is given back (as usual).

            :param: args: any argument
            :param: kwargs: any keyword argument
            :rtype: the result of the wrapped function
            """
            self.check_parameter_types(*args)
            result = function(*args, **kwargs)
            self.check_return_type(result)
            return result
        return wrapper

    def check_parameter_types(self, *args):
        """
        Validating parameter types.

        :param: args: any parameter (basically of the wrapped function)
        """
        parameter = self.offset
        for given_type, expected_type in \
                zip([type(entry) for entry in args[self.offset:]], self.parameter_types):
            if not given_type == expected_type:
                raise TypeError("%d. parameter - expected type is %s, given type is %s"
                                % (parameter, expected_type, given_type))
            parameter += 1

    def check_return_type(self, value):
        """
        Validating return type.

        :param: value: return value of wrapped function
        """
        if not type(value) == self.return_type:
            raise TypeError("expected return type is %s, given return type is %s"
                            % (self.return_type, type(value)))


def singleton(the_class):
    """Decorator for a class to make a singleton out of it."""
    class_instances = {}

    def get_instance(*args, **kwargs):
        """
        Creating or just return the one and only class instance.

        The singleton depends on the parameters used in __init__
        """
        key = (the_class, args, str(kwargs))
        if key not in class_instances:
            class_instances[key] = the_class(*args, **kwargs)
        return class_instances[key]

    return get_instance

"""
   Decorator tools.

.. module:: decorator
    :platform: Unix, Windows
    :synopis: decorator tools.

.. moduleauthor:: <NAME> <<EMAIL>>


   =======
   License
   =======
   Copyright (c) 2015 <NAME>

   Permission is hereby granted, free of charge, to any person obtaining a copy of this
   software and associated documentation files (the "Software"), to deal in the Software
   without restriction, including without limitation the rights to use, copy, modify, merge,
   publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
   to whom the Software is furnished to do so, subject to the following conditions:

   The above copyright notice and this permission notice shall be included in all copies
   or substantial portions of the Software.

   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
   INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
   IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
   DAMAGES OR OTHER LIABILITY,
   WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
   OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
import sys
import json
import inspect

if sys.version.startswith("2."):
    from types import NoneType
else:
    NoneType = type(None)


class data(object):
    """
    Decorator to provide data to a functor or method.

    The default is to get a parameter with name 'values' of type 'list'
    in your final function or method. For consistency reason this parameter
    will ALWAYS be a list.

    You can change the name using the parameter 'key'.
    Instead of using decorator parameter 'values' you can use 'file' to load
    the content of a file. If the filename ends with .json then it is
    loaded as json file.

    With parameter single you can run test decorated method as many entries
    are contained in the list.

    >>> @data(values=[1, 2, 3, 4])
    ... def test1(values): print(values)
    >>> test1()
    [1, 2, 3, 4]
    >>> @data(key='some_data', values=[4, 3, 2, 1])
    ... def test2(some_data): print(some_data)
    >>> test2()
    [4, 3, 2, 1]
    >>> @data(key='some_data', values=[2, 1], single=True)
    ... def test3(some_data): print(some_data)
    >>> test3()
    [2]
    [1]
    """

    def __init__(self, **kwargs):
        """
        Check the decorator parameters and keep the data for the call of the decorator.

        - valid parameters are 'key', 'file' and 'values'
        - you either can use 'file' OR 'values'
        - A file will be simple read as it is or as JSON if the name ends with '.json'
        """
        for key in kwargs:
            assert key in ['key', 'file', 'values', 'single']

        self.key = str(kwargs['key']) if 'key' in kwargs else "values"
        assert isinstance(self.key, str)
        self.values = kwargs['values'] if 'values' in kwargs else []
        assert isinstance(self.values, list)
        self.single = bool(kwargs['single']) if 'single' in kwargs else False

        if 'file' in kwargs:
            assert len(self.values) == 0
            if kwargs['file'].endswith(".json"):
                self.values.append(json.loads(open(kwargs['file'], 'r').read()))
            else:
                self.values.append(open(kwargs['file'], 'r').read())

    def __call__(self, function):
        """
        Providing the decorator for the function/method.

        When calling the decorator the defined values will
        be passed to the original function.

        :returns: decorator.
        """
        def decorator(*args):
            """The decorator function."""
            if self.single:
                for value in self.values:
                    arguments = {self.key: [value]}
                    function(*args, **arguments)
            else:
                arguments = {self.key: self.values}
                return function(*args, **arguments)

        decorator.__name__ = function.__name__
        decorator.__doc__ = function.__doc__
        return decorator


class validate_test_responsibility_for(object):
    """
    Decorator to ensure to test all methods for a given class.

    a class decorator that throws an exception when the test class does not
    implement all tests for all methods of the testable class (unit).
    The next code gives you an example on how it is used and what does happen:

    >>> class Value:
    ...     def __init__(self, value):
    ...         self.value = value
    ...
    >>> try:
    ...     import unittest
    ...     @validate_test_responsibility_for(Value)
    ...     class TestValue(unittest.TestCase):
    ...         pass
    ... except Exception as e:
    ...     print("|%s|" % str(e).strip())
    |...failed to provide test method 'TestValue.test_init' for method 'Value.__init__'|
    """

    def __init__(self, testable_class, include_class_name=False):
        """Store the class for test and checks all methods of that class."""
        if hasattr(testable_class, "decorated_object"):
            testable_class = testable_class.decorated_object

        self.testable_class = testable_class
        self.include_class_name = include_class_name
        self.methods_in_testable_class\
            = self.get_entries(self.testable_class, inspect.isfunction)\
            + self.get_entries(self.testable_class, inspect.ismethod)

    @staticmethod
    def get_entries(the_class, mode):
        """
        Query functions and method of a given class.

        get all entries by given mode (function or method) but the
        members of the concrete class only; not from its base
        """
        classes = {}
        for concrete_class in reversed(inspect.getmro(the_class)):
            classes[concrete_class] = {}
            for name, definition in dict(inspect.getmembers(concrete_class, mode)).items():
                if name.find("subclass") >= 0:
                    continue

                object_name = name
                is_base_method_only = False

                for known_class in classes:
                    if object_name not in classes[known_class]:
                        continue

                    if classes[known_class][object_name] == definition:
                        is_base_method_only = True
                        break

                if not is_base_method_only:
                    classes[concrete_class][object_name] = definition

        return list(classes[the_class].keys())

    def __call__(self, test_class):
        """Xalled when instantiated; then we have to verify for the required test methods."""
        self.verify(test_class)
        return test_class

    @staticmethod
    def get_test_method(name, prefix=""):
        """Adjusting final test method name."""
        # no underscores wanted (change "__init__" => "init")
        final_name = name.strip("_").lower()

        # ensure more readable name (like "equal" instead of "eq")
        if name == "__eq__":
            final_name = "equal"
        elif name == "__lt__":
            final_name = "less"
        elif name == "__gt__":
            final_name = "greater"

        return "test_" + prefix.lower() + final_name

    def verify(self, test_class):
        """Verification that for each testable method a test method does exist."""
        methods_in_test_class\
            = self.get_entries(test_class, inspect.isfunction)\
            + self.get_entries(test_class, inspect.ismethod)

        missing = []
        for testable_method in self.methods_in_testable_class:
            prefix = ""
            if self.include_class_name:
                prefix = self.testable_class.__name__ + "_"

            test_method = self.get_test_method(testable_method, prefix)
            if test_method in methods_in_test_class:
                continue

            missing.append((test_class.__name__ + "." + test_method,
                            self.testable_class.__name__ + "." + testable_method))

        if len(missing) > 0:
            # creates message with all missing methods throwing an exception for it
            message = ""
            for test_method, testable_method in missing:
                message += "\n...failed to provide test method '%s' for method '%s'" \
                           % (test_method, testable_method)
            raise Exception(message)


class validate_types(object):
    """
    A function or method decorator that validates types of parameters.

    >>> try:
    ...     @validate_types(ptypes=[int, int], r=int)
    ...     def sum_of_two_integers(a, b):
    ...         return a + b
    ...     sum_of_two_integers(1, "world")
    ... except TypeError as e:
    ...     print("|%s|" % str(e).replace('<class', '<type'))
    |1. parameter - expected type is <type 'int'>, given type is <type 'str'>|
    """

    def __init__(self, ptypes=[], r=NoneType, offset=0):
        """
        Register assertion information.

        :param: ptypes: list of parameter types like "int", "float" or "str"
        :param: e: return type
        :param: offset: number of parameters to ignore (from the beginning
        """
        self.parameter_types = ptypes
        self.return_type = r
        self.offset = offset

    def __call__(self, function):
        """
        Validating types.

        :param: function: that's the real function which should be decorated
        :rtype: decorator function (wrapper)
        """
        def wrapper(*args, **kwargs):
            """
            The wrapper checks the types of the parameter of the real function.

            It raises an exception if the type doesn't fit. If fine the
            real function is executed and the return value is given back (as usual).

            :param: args: any argument
            :param: kwargs: any keyword argument
            :rtype: the result of the wrapped function
            """
            self.check_parameter_types(*args)
            result = function(*args, **kwargs)
            self.check_return_type(result)
            return result
        return wrapper

    def check_parameter_types(self, *args):
        """
        Validating parameter types.

        :param: args: any parameter (basically of the wrapped function)
        """
        parameter = self.offset
        for given_type, expected_type in \
                zip([type(entry) for entry in args[self.offset:]], self.parameter_types):
            if not given_type == expected_type:
                raise TypeError("%d. parameter - expected type is %s, given type is %s"
                                % (parameter, expected_type, given_type))
            parameter += 1

    def check_return_type(self, value):
        """
        Validating return type.

        :param: value: return value of wrapped function
        """
        if not type(value) == self.return_type:
            raise TypeError("expected return type is %s, given return type is %s"
                            % (self.return_type, type(value)))


def singleton(the_class):
    """Decorator for a class to make a singleton out of it."""
    class_instances = {}

    def get_instance(*args, **kwargs):
        """
        Creating or just return the one and only class instance.

        The singleton depends on the parameters used in __init__
        """
        key = (the_class, args, str(kwargs))
        if key not in class_instances:
            class_instances[key] = the_class(*args, **kwargs)
        return class_instances[key]

    return get_instance
Decorator tools.  .. module:: decorator     :platform: Unix, Windows     :synopis: decorator tools.  .. moduleauthor:: <NAME> <<EMAIL>>      =======    License    =======    Copyright (c) 2015 <NAME>     Permission is hereby granted, free of charge, to any person obtaining a copy of this    software and associated documentation files (the "Software"), to deal in the Software    without restriction, including without limitation the rights to use, copy, modify, merge,    publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons    to whom the Software is furnished to do so, subject to the following conditions:     The above copyright notice and this permission notice shall be included in all copies    or substantial portions of the Software.     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,    INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.    IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,    DAMAGES OR OTHER LIABILITY,    WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Decorator to provide data to a functor or method.      The default is to get a parameter with name 'values' of type 'list'     in your final function or method. For consistency reason this parameter     will ALWAYS be a list.      You can change the name using the parameter 'key'.     Instead of using decorator parameter 'values' you can use 'file' to load     the content of a file. If the filename ends with .json then it is     loaded as json file.      With parameter single you can run test decorated method as many entries     are contained in the list.      >>> @data(values=[1, 2, 3, 4])     ... def test1(values): print(values)     >>> test1()     [1, 2, 3, 4]     >>> @data(key='some_data', values=[4, 3, 2, 1])     ... def test2(some_data): print(some_data)     >>> test2()     [4, 3, 2, 1]     >>> @data(key='some_data', values=[2, 1], single=True)     ... def test3(some_data): print(some_data)     >>> test3()     [2]     [1] Check the decorator parameters and keep the data for the call of the decorator.          - valid parameters are 'key', 'file' and 'values'         - you either can use 'file' OR 'values'         - A file will be simple read as it is or as JSON if the name ends with '.json' Providing the decorator for the function/method.          When calling the decorator the defined values will         be passed to the original function.          :returns: decorator. The decorator function. Decorator to ensure to test all methods for a given class.      a class decorator that throws an exception when the test class does not     implement all tests for all methods of the testable class (unit).     The next code gives you an example on how it is used and what does happen:      >>> class Value:     ...     def __init__(self, value):     ...         self.value = value     ...     >>> try:     ...     import unittest     ...     @validate_test_responsibility_for(Value)     ...     class TestValue(unittest.TestCase):     ...         pass     ... except Exception as e:     ...     print("|%s|" % str(e).strip())     |...failed to provide test method 'TestValue.test_init' for method 'Value.__init__'| Store the class for test and checks all methods of that class. Query functions and method of a given class.          get all entries by given mode (function or method) but the         members of the concrete class only; not from its base Xalled when instantiated; then we have to verify for the required test methods. Adjusting final test method name. # no underscores wanted (change "__init__" => "init") # ensure more readable name (like "equal" instead of "eq") Verification that for each testable method a test method does exist. # creates message with all missing methods throwing an exception for it A function or method decorator that validates types of parameters.      >>> try:     ...     @validate_types(ptypes=[int, int], r=int)     ...     def sum_of_two_integers(a, b):     ...         return a + b     ...     sum_of_two_integers(1, "world")     ... except TypeError as e:     ...     print("|%s|" % str(e).replace('<class', '<type'))     |1. parameter - expected type is <type 'int'>, given type is <type 'str'>| Register assertion information.          :param: ptypes: list of parameter types like "int", "float" or "str"         :param: e: return type         :param: offset: number of parameters to ignore (from the beginning Validating types.          :param: function: that's the real function which should be decorated         :rtype: decorator function (wrapper) The wrapper checks the types of the parameter of the real function.              It raises an exception if the type doesn't fit. If fine the             real function is executed and the return value is given back (as usual).              :param: args: any argument             :param: kwargs: any keyword argument             :rtype: the result of the wrapped function Validating parameter types.          :param: args: any parameter (basically of the wrapped function) Validating return type.          :param: value: return value of wrapped function Decorator for a class to make a singleton out of it. Creating or just return the one and only class instance.          The singleton depends on the parameters used in __init__