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