nstarman/utilipy

View on GitHub
utilipy/decorators/code_dev.py

Summary

Maintainability
B
5 hrs
Test Coverage
# -*- coding: utf-8 -*-
# Licensed under a 3-clause BSD style license - see LICENSE.rst


"""Decorators for code in active development.

See Also
--------
deprecation decorators in :function:`~astropy.util.exceptions.deprecated`,
upon which this code is based.

"""

__all__ = [
    "indev",
    "indev_doc",
    "indev_attribute",
]


##############################################################################
# IMPORTS

# BUILT-IN
import functools
import inspect
import textwrap
import types
import typing as T
import warnings

# PROJECT-SPECIFIC
from utilipy.utils.exceptions import utilipyWarning

##############################################################################
# PARAMETERS

method_types = (classmethod, staticmethod, types.MethodType)

##############################################################################
# CODE
##############################################################################


class DevelopmentWarning(utilipyWarning):
    """Warning class to indicate a feature as being actively developed.

    Expect the feature to change in input, output and implementation.

    """


# /class


class BetaDevelopmentWarning(DevelopmentWarning):
    """Warning class to indicate an upcoming feature that is nearly ready."""


# /class

##############################################################################


def _get_function(func):
    """Get function object given function.

    Returns
    -------
    func : Callable
        The function from the function wrapper.

    """
    if isinstance(func, method_types):
        func = func.__func__
    return func


# /def


# -------------------------------------------------------------------


def indev_function(func: T.Callable, message: str, warning_type):
    """Wrap function to display ``warning_type`` when called.

    Parameters
    ----------
    func : Callable
    message : str
        The warning message
    warning_type : type
        Warning class.

    Returns
    -------
    Callable
        Wrapped function.

    """
    if isinstance(func, method_types):
        func_wrapper = type(func)
    else:

        def func_wrapper(func):
            return func

    func = _get_function(func)

    def upcoming_func(*args, **kwargs):
        warnings.warn(message, warning_type, stacklevel=2)

        return func(*args, **kwargs)

    # If this is an extension function, we can't call
    # functools.wraps on it, but we normally don't care.
    # This crazy way to get the type of a wrapper descriptor is
    # straight out of the Python 3.3 inspect module docs.
    if type(func) is not type(str.__dict__["__add__"]):  # noqa
        upcoming_func = functools.wraps(func)(upcoming_func)

    upcoming_func.__doc__ = indev_doc(upcoming_func.__doc__, message)

    return func_wrapper(upcoming_func)


# /def


# -------------------------------------------------------------------


def indev_class(cls, message: str, warning_type):
    """Class in-dev.

    Update the docstring and wrap the ``__init__`` in-place (or ``__new__``
    if the class or any of the bases overrides ``__new__``) so it will give
    an in-dev warning when an instance is created.

    This won't work for extension classes because these can't be modified
    in-place and the alternatives don't work in the general case:

    - Using a new class that looks and behaves like the original doesn't
      work because the __new__ method of extension types usually makes sure
      that it's the same class or a subclass.
    - Subclassing the class and return the subclass can lead to problems
      with pickle and will look weird in the Sphinx docs.

    Returns
    -------
    cls : ClassType

    """
    cls.__doc__ = indev_doc(cls.__doc__, message)

    if cls.__new__ is object.__new__:
        cls.__init__ = indev_function(
            _get_function(cls.__init__),
            message=message,
            warning_type=warning_type,
        )
    else:
        cls.__new__ = indev_function(
            _get_function(cls.__new__),
            message=message,
            warning_type=warning_type,
        )

    return cls


# /def

##############################################################################


def indev_doc(old_doc: T.Union[str, None], message: str):
    """Returns a given docstring with an in-dev message prepended.

    .. todo::

        process docstring better

    Parameters
    ----------
    old_doc: str
    message : str

    Returns
    -------
    new_doc : str

    """
    if not old_doc:  # empty str or None
        old_doc = ""

    old_doc = textwrap.dedent(old_doc).strip("\n")

    note_indev = (
        "\n    .. versionchanged:: indev\n"
        "\n        {message}\n".format(**{"message": message.strip()})
    )

    # TODO, process docstring better
    _sections = [
        "\n    Parameters\n    ----------",
        "\n    Returns\n    -------",
    ]
    if _sections[0] in old_doc:  # check if has Parameters
        descr, rest = old_doc.split(_sections[0])
        descr += note_indev
        new_doc = descr + _sections[0] + rest

    elif _sections[1] in old_doc:  # maybe starts with Returns
        descr, rest = old_doc.split(_sections[1])
        descr += note_indev
        new_doc = descr + _sections[1] + rest

    else:  # just a regular doc
        new_doc = old_doc + "\n" + note_indev + "\n"

    return new_doc


# /def


# -------------------------------------------------------------------


def indev(
    message: T.Union[str, T.Callable] = "",
    name: str = "",
    alternative: str = "",
    todo: str = "",
    obj_type: T.Optional[str] = None,
    warning_type=DevelopmentWarning,
):
    """Used to mark a function or class as in the development phase.

    Expect the feature to change in input, output and implementation.

    To mark an attribute as upcoming / in development, use `indev_attribute`.

    Parameters
    ----------
    message : str, optional
        Override the default in-dev message.  The format
        specifier ``func`` may be used for the name of the function,
        and ``alternative`` may be used in the in-dev message
        to insert the name of an alternative to the in-dev
        function. ``obj_type`` may be used to insert a friendly name
        for the type of object being in-dev.

    name : str, optional
        The name of the in-dev function or class; if not provided
        the name is automatically determined from the passed in
        function or class, though this is useful in the case of
        renamed functions, where the new function is just assigned to
        the name of the in-dev function.  For example::

            def new_function():
                ...
            oldFunction = new_function

    alternative : str, optional
        An alternative function or class name that the user may use in
        place of the in-dev object.  The in-dev warning will
        tell the user about this alternative if provided.

    todo : str, optional
        A todo about intended features and functionality. The in-dev warning
        will tell the user about this alternative if provided.

    obj_type : str, optional
        The type of this object, if the automatically determined one
        needs to be overridden.

    warning_type : warning
        Warning to be issued.
        Default is `~DevelopmentWarning`.


    See Also
    --------
    deprecation decorators in :func:`~astropy.util.exceptions.deprecated`

    """

    # wrapper function
    def make_indev(
        obj,
        message=message,
        name=name,
        alternative=alternative,
        warning_type=warning_type,
    ):
        """Mark object as in-development.

        Parameters
        ----------
        obj : object
        message : str
        name : str
        alternative : str
        warning_type : ClassType

        Returns
        -------
        Callable

        """
        if obj_type is None:
            if isinstance(obj, type):
                obj_type_name = "class"
            elif inspect.isfunction(obj):
                obj_type_name = "function"
            elif inspect.ismethod(obj) or isinstance(obj, method_types):
                obj_type_name = "method"
            else:
                obj_type_name = "object"
        else:
            obj_type_name = obj_type

        if not name:
            name = _get_function(obj).__name__

        altmessage = ""
        todomessage = ""
        if not message or (type(message) is type(make_indev)):
            message = (
                "The {func} {obj_type} is in development and may "
                "be added in a future version."
            )
            if alternative:
                altmessage = f"\n        Use {alternative} instead."
            if todo:
                todomessage = f"\n        TODO: {todo}"

        message = (
            message.format(
                **{
                    "func": name,
                    "name": name,
                    "alternative": alternative,
                    "todo": todo,
                    "obj_type": obj_type_name,
                }
            )
            + altmessage
            + todomessage
        )

        if isinstance(obj, type):  # decorate class
            return indev_class(obj, message=message, warning_type=warning_type)
        else:  # decorate function
            return indev_function(
                obj, message=message, warning_type=warning_type
            )

    # /def

    if type(message) is type(make_indev):
        return make_indev(message)

    return make_indev


# /def


# ------------------------------------------------------------------------


def indev_attribute(
    name,
    message=None,
    alternative=None,
    todo=None,
    warning_type=DevelopmentWarning,
):
    """Mark a public attribute as in development.

    This creates a property that will warn when the given attribute name
    is accessed. To prevent the warning (i.e. for internal code),
    use the private name for the attribute by prepending an underscore
    (i.e. ``self._name``).

    Parameters
    ----------
    name : str
        The name of the in-dev attribute.

    message : str, optional
        Override the default in-dev message.  The format
        specifier ``name`` may be used for the name of the attribute,
        and ``alternative`` may be used in the in-dev message
        to insert the name of an alternative to the in-dev
        function.

    alternative : str, optional
        An alternative attribute that the user may use in place of the
        in-dev attribute.  The in-dev warning will tell the
        user about this alternative if provided.

    todo : str, optional
        A todo about intended functionality. The in-dev warning
        will tell the user about this alternative if provided.

    warning_type : warning
        Warning to be issued.
        Default is `~DevelopmentWarning`.

    Examples
    --------
    ::

        class MyClass:
            # Mark the new_attr as in development
            new_attr = misc.indev_attribute('new_attr')

            def method(self):
                self._new_attr = 42

    Returns
    -------
    property

    """
    private_name = "_" + name

    @indev(
        name=name,
        obj_type="attribute",
        warning_type=warning_type,
        alternative=alternative,  # TODO, implement for attribute
        todo=todo,  # TODO, implement for attribute
    )
    def get(self):
        return getattr(self, private_name)

    @indev(
        name=name,
        obj_type="attribute",
        warning_type=warning_type,
        alternative=alternative,  # TODO, implement for attribute
        todo=todo,  # TODO, implement for attribute
    )
    def set(self, val):
        setattr(self, private_name, val)

    @indev(
        name=name,
        obj_type="attribute",
        warning_type=warning_type,
        alternative=alternative,  # TODO, implement for attribute
        todo=todo,  # TODO, implement for attribute
    )
    def delete(self):
        delattr(self, private_name)

    return property(get, set, delete)


# /def


##############################################################################
# END