# Source code for canlib.deprecation

    """helper module for handling deprecation via the standard warnings module

    The first time any function marked in this module is called, a RuntimeWarning
    will be issued. This is because DeprecationWarnings are hidden by default while
    RuntimeWarnings are not. The RuntimeWarning explains to run python with the -Wd
    flag to show DeprecationWarnings.

    Note:

        All decorators in this module (and most other) must be placed before
        (below) any `staticmethod` and `classmethod` decorators, as those
        decorators do not wrap the functions well enough for `deprecation` to
        function.

    To simply mark a function as deprecated, just decorate it with this module's
    `deprecated` decorator::

      @deprecated
      def function():
        ...

    This will produce a simple warning that the function is deprecated. If you want
    to be a little more helpful, you can also tell your users what function to use
    instead::

      @deprecated.favour("new_function")
      def function():
        ...

    This will produce a warning that tells the user to use whatever is supplied as
    the first argument instead of the decorated function.

    Alternatively, in the case of a function being renamed, one can use::

      @deprecated.replacedby(new_function):
      def function():
        pass

    This will completely replace the decorated function with a dummy function that
    first warns about the use of the deprecated name, and then calls the function
    supplied as the first argument to the decorator.

    If only certain uses a function are deprecated the `manual_warn` function can
    be called when something deprecated is used, passing the full message to be
    displayed::

      def function(arg=None):
        if arg is None:
           manual_warn("Calling function without arg is deprecated")

    If classes are renamed, their old names can be deprecated with::

      Class = class_replaced("Class", NewClass)

    Likewise, class attributes can be deprecated with::

      class Class:
        attr = attr_replaced("attr", "new_attr")

    """

    import warnings
    import functools
    import textwrap

    class KvWarning(Warning):
        """Base class for all canlib warnings"""
        pass

    class KvDeprecationBase(Warning):
        """Base class for canlib warnings related to deprecation"""
        pass

    class KvDeprecationWarning(KvDeprecationBase, DeprecationWarning):
        """canlib equivalent of standard DeprecationWarning"""
        pass

    class KvDeprecatedUsage(KvDeprecationBase, RuntimeWarning):
        """Special RuntimeWarning emitted on first KvDeprecationWarning"""
        pass

    def manual_warn(message, stacklevel=3):
        """Manually warn using this module

        Any calls to this function qualifies as the "first" call in terms of
        raising a KvDeprecatedUsage.

        Args:
            message (str): used to initialize a KvDeprecationWarning
            stacklevel (int) [optional]: passed on to `warnings.warn`

        """
        deprecated._any_called()
        warnings.warn(KvDeprecationWarning(message), stacklevel=stacklevel)

    def attr_replaced(original, replacement):
        """Create a `property` object representing a deprecated attribute

        Provides a getter, setter, and docstring.

        Args:
            original (str): the name of the deprecated attribute
            replacement (str): the name of the new attribute, to be used instead
        """
        def fget(self):
            manual_warn("Accessing {o} is deprecated, use {r} instead".format(
                o=original, r=replacement))
            return getattr(self, replacement)

        def fset(self, val):
            manual_warn("Accessing {o} is deprecated, use {r} instead".format(
                o=original, r=replacement))
            setattr(self, replacement, val)

        doc = "Deprecated name for `%s`" % replacement

        return property(fget=fget, fset=fset, doc=doc)

    def class_replaced(original, new, replacement=None):
        """Create a deprecated class that preserves backwards compatibility

        Args:
            original (str): the name of the deprecated class
            new: the new class that replaces this one
            replacement (str) [optional]: the name of the new class, by default
                inferred from `new` argument
        """
        if replacement is None:
            replacement = new.__name__

        def init_moved_class(self, *args, **kwargs):
            manual_warn("{old} is deprecated, please use {new} instead".format(
                old=original,
                new=replacement,
            ))
            return new.__init__(self, *args, **kwargs)

        docparagraph = (
            "    `{old}` has been renamed `{new}`, using the old name (`{old}`) is deprecated."
        ).format(old=original, new=replacement)

        docparagraph = textwrap.fill(docparagraph, width=79, subsequent_indent="    ")

        docstring = "Deprecated name for `{new}`\n\n{para}\n\n    ".format(
            para=docparagraph, new=replacement)

        newcls = type(original, (new, object), {'__init__': init_moved_class,
                                                '__doc__': docstring})

        return newcls

    class deprecated(object):
        """Decorator for deprecating functions

        Also provides several class methods for alternate ways to wrap functions
        (or classes).

        """
        _replacement = None
        _first = True

        @classmethod
        def cls(cls, replacement):
            """Wrap a class's `__init__` function as deprecated

            Args:
                replacement (str): the name of the new class
            """
            def expanded(old):
                obj = cls(old.__init__)
                old.__init__ = obj
                obj.__name__ = old.__name__
                obj._replacement = replacement
                return obj
            return expanded

        @classmethod
        def favour(cls, replacement):
            """Wraps a function as deprecated

            Args:
                replacement (str): the name of the new function
            """
            def expanded(func):
                obj = cls(func)
                obj._replacement = replacement
                return obj
            return expanded

        @classmethod
        def replacedby(cls, new, replacement=None):
            """Wraps a function as deprecated, completely replacing it

            A function wrapped with this is never actually executed; when called,
            the call is delegated to the new function instead.

            Args:
                original (str): the name of the deprecated function
                new: the new function that replaces this one
                replacement (str) [optional]: the name of the new function, by default
                    inferred from `new` argument

            """
            def expanded(old):
                obj = cls(old)
                obj.func = new
                obj._replacement = replacement or new.__name__
                return obj
            return expanded

        def __init__(self, func):
            """Wraps a function as deprecated"""
            self.func = func
            functools.update_wrapper(self, func)

        def __call__(self, *args, **kwargs):
            self._any_called()
            if self._replacement is None:
                msg = self.__name__ + " has been deprecated!"
            else:
                msg = "{f} has been deprecated, use {r} instead!".format(
                    f=self.__name__,
                    r=self._replacement,
                )
            warnings.warn(KvDeprecationWarning(msg), stacklevel=2)
            ret = self.func(*args, **kwargs)
            return ret

        def __get__(self, instance, owner):
            if instance is None:
                return self
            else:
                return functools.partial(self, instance)

        @classmethod
        def _any_called(cls):
            if cls._first:
                warnings.warn(KvDeprecatedUsage(
                    "A deprecated function was called! " +
                    "Run python with -Wd flag for more information."))
                cls._first = False