1 27
import sys
2 27
from functools import wraps
3 27
from types import ModuleType
4 27
import warnings
5

6 27
import attr
7

8

9
# We want our warnings to be visible by default (at least for now), but we
10
# also want it to be possible to override that using the -W switch. AFAICT
11
# this means we cannot inherit from DeprecationWarning, because the only way
12
# to make it visible by default then would be to add our own filter at import
13
# time, but that would override -W switches...
14 27
class TrioDeprecationWarning(FutureWarning):
15
    """Warning emitted if you use deprecated Trio functionality.
16

17
    As a young project, Trio is currently quite aggressive about deprecating
18
    and/or removing functionality that we realize was a bad idea. If you use
19
    Trio, you should subscribe to `issue #1
20
    <https://github.com/python-trio/trio/issues/1>`__ to get information about
21
    upcoming deprecations and other backwards compatibility breaking changes.
22

23
    Despite the name, this class currently inherits from
24
    :class:`FutureWarning`, not :class:`DeprecationWarning`, because while
25
    we're in young-and-aggressive mode we want these warnings to be visible by
26
    default. You can hide them by installing a filter or with the ``-W``
27
    switch: see the :mod:`warnings` documentation for details.
28

29
    """
30

31

32 27
def _url_for_issue(issue):
33 27
    return "https://github.com/python-trio/trio/issues/{}".format(issue)
34

35

36 27
def _stringify(thing):
37 27
    if hasattr(thing, "__module__") and hasattr(thing, "__qualname__"):
38 27
        return "{}.{}".format(thing.__module__, thing.__qualname__)
39 27
    return str(thing)
40

41

42 27
def warn_deprecated(thing, version, *, issue, instead, stacklevel=2):
43 27
    stacklevel += 1
44 27
    msg = "{} is deprecated since Trio {}".format(_stringify(thing), version)
45 27
    if instead is None:
46 27
        msg += " with no replacement"
47
    else:
48 27
        msg += "; use {} instead".format(_stringify(instead))
49 27
    if issue is not None:
50 27
        msg += " ({})".format(_url_for_issue(issue))
51 27
    warnings.warn(TrioDeprecationWarning(msg), stacklevel=stacklevel)
52

53

54
# @deprecated("0.2.0", issue=..., instead=...)
55
# def ...
56 27
def deprecated(version, *, thing=None, issue, instead):
57 27
    def do_wrap(fn):
58
        nonlocal thing
59

60 27
        @wraps(fn)
61 13
        def wrapper(*args, **kwargs):
62 27
            warn_deprecated(thing, version, instead=instead, issue=issue)
63 27
            return fn(*args, **kwargs)
64

65
        # If our __module__ or __qualname__ get modified, we want to pick up
66
        # on that, so we read them off the wrapper object instead of the (now
67
        # hidden) fn object
68 27
        if thing is None:
69 27
            thing = wrapper
70

71 27
        if wrapper.__doc__ is not None:
72 27
            doc = wrapper.__doc__
73 27
            doc = doc.rstrip()
74 27
            doc += "\n\n"
75 27
            doc += ".. deprecated:: {}\n".format(version)
76 27
            if instead is not None:
77 27
                doc += "   Use {} instead.\n".format(_stringify(instead))
78 27
            if issue is not None:
79 27
                doc += "   For details, see `issue #{} <{}>`__.\n".format(
80
                    issue, _url_for_issue(issue)
81
                )
82 27
            doc += "\n"
83 27
            wrapper.__doc__ = doc
84

85 27
        return wrapper
86

87 27
    return do_wrap
88

89

90 27
def deprecated_alias(old_qualname, new_fn, version, *, issue):
91 27
    @deprecated(version, issue=issue, instead=new_fn)
92 27
    @wraps(new_fn, assigned=("__module__", "__annotations__"))
93 13
    def wrapper(*args, **kwargs):
94
        "Deprecated alias."
95 27
        return new_fn(*args, **kwargs)
96

97 27
    wrapper.__qualname__ = old_qualname
98 27
    wrapper.__name__ = old_qualname.rpartition(".")[-1]
99 27
    return wrapper
100

101

102 27
@attr.s(frozen=True)
103 13
class DeprecatedAttribute:
104 27
    _not_set = object()
105

106 27
    value = attr.ib()
107 27
    version = attr.ib()
108 27
    issue = attr.ib()
109 27
    instead = attr.ib(default=_not_set)
110

111

112 27
class _ModuleWithDeprecations(ModuleType):
113 27
    def __getattr__(self, name):
114 27
        if name in self.__deprecated_attributes__:
115 27
            info = self.__deprecated_attributes__[name]
116 27
            instead = info.instead
117 27
            if instead is DeprecatedAttribute._not_set:
118 27
                instead = info.value
119 27
            thing = "{}.{}".format(self.__name__, name)
120 27
            warn_deprecated(thing, info.version, issue=info.issue, instead=instead)
121 27
            return info.value
122

123 27
        msg = "module '{}' has no attribute '{}'"
124 27
        raise AttributeError(msg.format(self.__name__, name))
125

126

127 27
def enable_attribute_deprecations(module_name):
128 27
    module = sys.modules[module_name]
129 27
    module.__class__ = _ModuleWithDeprecations
130
    # Make sure that this is always defined so that
131
    # _ModuleWithDeprecations.__getattr__ can access it without jumping
132
    # through hoops or risking infinite recursion.
133 27
    module.__deprecated_attributes__ = {}

Read our documentation on viewing source code .

Loading