import sys
from functools import wraps
from types import ModuleType
import warnings
import attr
# We want our warnings to be visible by default (at least for now), but we
# also want it to be possible to override that using the -W switch. AFAICT
# this means we cannot inherit from DeprecationWarning, because the only way
# to make it visible by default then would be to add our own filter at import
# time, but that would override -W switches...
class TrioDeprecationWarning(FutureWarning):
"""Warning emitted if you use deprecated Trio functionality.
As a young project, Trio is currently quite aggressive about deprecating
and/or removing functionality that we realize was a bad idea. If you use
Trio, you should subscribe to `issue #1
<https://github.com/python-trio/trio/issues/1>`__ to get information about
upcoming deprecations and other backwards compatibility breaking changes.
Despite the name, this class currently inherits from
:class:`FutureWarning`, not :class:`DeprecationWarning`, because while
we're in young-and-aggressive mode we want these warnings to be visible by
default. You can hide them by installing a filter or with the ``-W``
switch: see the :mod:`warnings` documentation for details.
"""
def _url_for_issue(issue):
return "https://github.com/python-trio/trio/issues/{}".format(issue)
def _stringify(thing):
if hasattr(thing, "__module__") and hasattr(thing, "__qualname__"):
return "{}.{}".format(thing.__module__, thing.__qualname__)
return str(thing)
def warn_deprecated(thing, version, *, issue, instead, stacklevel=2):
stacklevel += 1
msg = "{} is deprecated since Trio {}".format(_stringify(thing), version)
if instead is None:
msg += " with no replacement"
else:
msg += "; use {} instead".format(_stringify(instead))
if issue is not None:
msg += " ({})".format(_url_for_issue(issue))
warnings.warn(TrioDeprecationWarning(msg), stacklevel=stacklevel)
# @deprecated("0.2.0", issue=..., instead=...)
# def ...
def deprecated(version, *, thing=None, issue, instead):
def do_wrap(fn):
nonlocal thing
@wraps(fn)
def wrapper(*args, **kwargs):
warn_deprecated(thing, version, instead=instead, issue=issue)
return fn(*args, **kwargs)
# If our __module__ or __qualname__ get modified, we want to pick up
# on that, so we read them off the wrapper object instead of the (now
# hidden) fn object
if thing is None:
thing = wrapper
if wrapper.__doc__ is not None:
doc = wrapper.__doc__
doc = doc.rstrip()
doc += "\n\n"
doc += ".. deprecated:: {}\n".format(version)
if instead is not None:
doc += " Use {} instead.\n".format(_stringify(instead))
if issue is not None:
doc += " For details, see `issue #{} <{}>`__.\n".format(
issue, _url_for_issue(issue)
)
doc += "\n"
wrapper.__doc__ = doc
return wrapper
return do_wrap
def deprecated_alias(old_qualname, new_fn, version, *, issue):
@deprecated(version, issue=issue, instead=new_fn)
@wraps(new_fn, assigned=("__module__", "__annotations__"))
def wrapper(*args, **kwargs):
"Deprecated alias."
return new_fn(*args, **kwargs)
wrapper.__qualname__ = old_qualname
wrapper.__name__ = old_qualname.rpartition(".")[-1]
return wrapper
@attr.s(frozen=True)
class DeprecatedAttribute:
_not_set = object()
value = attr.ib()
version = attr.ib()
issue = attr.ib()
instead = attr.ib(default=_not_set)
class _ModuleWithDeprecations(ModuleType):
def __getattr__(self, name):
if name in self.__deprecated_attributes__:
info = self.__deprecated_attributes__[name]
instead = info.instead
if instead is DeprecatedAttribute._not_set:
instead = info.value
thing = "{}.{}".format(self.__name__, name)
warn_deprecated(thing, info.version, issue=info.issue, instead=instead)
return info.value
msg = "module '{}' has no attribute '{}'"
raise AttributeError(msg.format(self.__name__, name))
def enable_attribute_deprecations(module_name):
module = sys.modules[module_name]
module.__class__ = _ModuleWithDeprecations
# Make sure that this is always defined so that
# _ModuleWithDeprecations.__getattr__ can access it without jumping
# through hoops or risking infinite recursion.
module.__deprecated_attributes__ = {}