1# -*- coding: utf-8 -*-
2
3import warnings
4
5
6class Deprecation(object):
7    """Decorator factory for deprecating functions or classes.
8
9    This class represent deprecations of functions or classes and is designed
10    to be used with the ``warnings`` library.
11
12    Parameters
13    ----------
14    last_supported_version : str, optional
15        Version string, e.g. ``'0.2.1'``.
16    will_be_missing_in : str, optional
17        Version string, e.g. ``'0.3.0'``.
18    use_instead : object or str, optional
19        Function or class to use instead or descriptive string.
20    issue : str, optional
21    issues_url : callback, optional
22        Converts issue to url, e.g. ``lambda s: 'https://github.com/user/repo/issues/%s/' % s.lstrip('gh-')``.
23    warning: DeprecationWarning, optional
24        Any subclass of DeprecationWarning, tip: you may invoke:
25        ``warnings.simplefilter('once', MyWarning)`` at module init.
26
27    Examples
28    --------
29    >>> import warnings
30    >>> warnings.simplefilter("error", DeprecationWarning)
31    >>> @Deprecation()
32    ... def f():
33    ...     return 1
34    ...
35    >>> f()  # doctest: +IGNORE_EXCEPTION_DETAIL
36    Traceback (most recent call last):
37    ...
38    DeprecationWarning: f is deprecated.
39    >>> @Deprecation(last_supported_version='0.4.0')
40    ... def some_old_function(x):
41    ...     return x*x - x
42    ...
43    >>> Deprecation.inspect(some_old_function).last_supported_version
44    '0.4.0'
45    >>> @Deprecation(will_be_missing_in='1.0')
46    ... class ClumsyClass(object):
47    ...     pass
48    ...
49    >>> Deprecation.inspect(ClumsyClass).will_be_missing_in
50    '1.0'
51    >>> warnings.resetwarnings()
52
53    Notes
54    -----
55    :class:`DeprecationWarning` is ignored by default. Use custom warning
56    and filter appropriately. Alternatively, run python with ``-W`` flag or set
57    the appropriate environment variable:
58
59    ::
60
61        $ python -c 'import warnings as w; w.warn("X", DeprecationWarning)'
62        $ python -Wd -c 'import warnings as w; w.warn("X", DeprecationWarning)'
63        -c:1: DeprecationWarning: X
64        $ export PYTHONWARNINGS=d
65        $ python -c 'import warnings as w; w.warn("X", DeprecationWarning)'
66        -c:1: DeprecationWarning: X
67
68    """
69
70    _deprecations = {}
71
72    def __init__(
73        self,
74        last_supported_version=None,
75        will_be_missing_in=None,
76        use_instead=None,
77        issue=None,
78        issues_url=None,
79        warning=DeprecationWarning,
80    ):
81        if (
82            last_supported_version is not None
83            and not isinstance(last_supported_version, (str, tuple, list))
84            and callable(last_supported_version)
85        ):
86            raise ValueError("last_supported_version not str, tuple or list")
87
88        self.last_supported_version = last_supported_version
89        self.will_be_missing_in = will_be_missing_in
90        self.use_instead = use_instead
91        self.issue = issue
92        self.issues_url = issues_url
93        self.warning = warning
94        self.warning_message = self._warning_message_template()
95
96    @classmethod
97    def inspect(cls, obj):
98        """Get the :class:`Deprecation` instance of a deprecated function."""
99        return cls._deprecations[obj]
100
101    def _warning_message_template(self):
102        msg = "%(func_name)s is deprecated"
103        if self.last_supported_version is not None:
104            msg += " since (not including) % s" % self.last_supported_version
105        if self.will_be_missing_in is not None:
106            msg += ", it will be missing in %s" % self.will_be_missing_in
107        if self.issue is not None:
108            if self.issues_url is not None:
109                msg += self.issues_url(self.issue)
110            else:
111                msg += " (see issue %s)" % self.issue
112        if self.use_instead is not None:
113            try:
114                msg += ". Use %s instead" % self.use_instead.__name__
115            except AttributeError:
116                msg += ". Use %s instead" % self.use_instead
117        return msg + "."
118
119    def __call__(self, wrapped):
120        """Decorates function to be deprecated"""
121        msg = self.warning_message % {"func_name": wrapped.__name__}
122        wrapped_doc = wrapped.__doc__ or ""
123        if hasattr(wrapped, "__mro__"):  # wrapped is a class
124
125            class _Wrapper(wrapped):
126                __doc__ = msg + "\n\n" + wrapped_doc
127
128                def __init__(_self, *args, **kwargs):
129                    warnings.warn(msg, self.warning, stacklevel=2)
130                    wrapped.__init__(_self, *args, **kwargs)
131
132        else:  # wrapped is a function
133
134            def _Wrapper(*args, **kwargs):
135                warnings.warn(msg, self.warning, stacklevel=2)
136                return wrapped(*args, **kwargs)
137
138            _Wrapper.__doc__ = msg + "\n\n" + wrapped_doc
139
140        self._deprecations[_Wrapper] = self
141        _Wrapper.__name__ = wrapped.__name__
142        _Wrapper.__module__ = wrapped.__module__
143        return _Wrapper
144