"""
A utility class for deprecating code.
"""
import functools
import types
import warnings
from copy import copy
[docs]
class Deprecator:
"""
Decorator class to mark functions and methods as deprecated with a uniform
warning message at the time the function is called. The message has the
form
{function_name} is deprecated: {message}. It is not guaranteed to be in
service after {version}.
unless `pending=True` was given. Then the message will be
{function_name} will be deprecated in a future version: {message}.
If message and version are not initialized or given during the decorating
call the respective parts are left out from the message.
>>> from pyiron_snippets.deprecate import Deprecator
>>>
>>> deprecate = Deprecator()
>>> @deprecate
... def foo(a, b):
... pass
>>> foo(1, 2) # doctest: +SKIP
DeprecationWarning: __main__.foo is deprecated
>>> @deprecate("use bar() instead")
... def foo(a, b):
... pass
>>> foo(1, 2) # doctest: +SKIP
DeprecationWarning: __main__.foo is deprecated: use bar instead
>>> @deprecate("use bar() instead", version="0.4.0")
... def foo(a, b):
... pass
>>> foo(1, 2) # doctest: +SKIP
DeprecationWarning: __main__.foo is deprecated: use bar instead. It is not guaranteed to be in service in vers. 0.4.0
>>> deprecate = Deprecator(message="I say no!", version="0.5.0")
>>> @deprecate
... def foo(a, b):
... pass
>>> foo(1, 2) # doctest: +SKIP
DeprecationWarning: __main__.foo is deprecated: I say no! It is not guaranteed to be in service in vers. 0.5.0`
Alternatively the decorator can also be called with `arguments` set to a dictionary
mapping names of keyword arguments to deprecation messages. In this case the
warning will only be emitted when the decorated function is called with arguments
in that dictionary.
>>> deprecate = Deprecator()
>>> @deprecate(arguments={"bar": "use baz instead."})
... def foo(bar=None, baz=None):
... pass
>>> foo(baz=True)
>>> foo(bar=True) # doctest: +SKIP
DeprecationWarning: __main__.foo(bar=True) is deprecated: use baz instead.
As a short-cut, it is also possible to pass the values in the arguments dict
directly as keyword arguments to the decorator.
>>> @deprecate(bar="use baz instead.")
... def foo(bar=None, baz=None):
... pass
>>> foo(baz=True)
>>> foo(bar=True) # doctest: +SKIP
DeprecationWarning: __main__.foo(bar=True) is deprecated: use baz instead.
"""
def __init__(self, message=None, version=None, pending=False):
"""
Initialize default values for deprecation message and version.
Args:
message (str): default deprecation message
version (str): default version after which the function might be removed
pending (bool): only warn about future deprecation, warning category will
be PendingDeprecationWarning instead of DeprecationWarning
"""
self.message = message
self.version = version
self.category = PendingDeprecationWarning if pending else DeprecationWarning
def __copy__(self):
cp = type(self)(message=self.message, version=self.version)
cp.category = self.category
return cp
def __call__(self, message=None, version=None, arguments=None, **kwargs):
depr = copy(self)
if isinstance(message, types.FunctionType):
return depr.__deprecate_function(message)
else:
depr.message = message
depr.version = version
depr.arguments = arguments if arguments is not None else {}
depr.arguments.update(kwargs)
return depr.wrap
def _build_message(self):
if self.category is PendingDeprecationWarning:
message_format = "{} will be deprecated"
else:
message_format = "{} is deprecated"
if self.message is not None:
message_format += f": {self.message}."
else:
message_format += "."
if self.version is not None:
message_format += (
f" It is not guaranteed to be in service in vers. {self.version}"
)
return message_format
def __deprecate_function(self, function):
message = self._build_message().format(
f"{function.__module__}.{function.__name__}"
)
@functools.wraps(function)
def decorated(*args, **kwargs):
warnings.warn(message, category=self.category, stacklevel=2)
return function(*args, **kwargs)
return decorated
def __deprecate_argument(self, function):
message_format = self._build_message()
@functools.wraps(function)
def decorated(*args, **kwargs):
for kw in kwargs:
if kw in self.arguments:
warnings.warn(
message_format.format(
f"{function.__module__}.{function.__qualname__}"
f"({kw}={kwargs[kw]})"
),
category=self.category,
stacklevel=2,
)
return function(*args, **kwargs)
return decorated
[docs]
def wrap(self, function):
"""
Wrap the given function to emit a DeprecationWarning at call time. The warning
message is constructed from the given message and version. If
:attr:`.arguments` is set then the warning is only emitted, when the decorated
function is called with keyword arguments found in that dictionary.
Args:
function (callable): function to mark as deprecated
Return:
function: raises DeprecationWarning when given function is called
"""
if not self.arguments:
return self.__deprecate_function(function)
else:
return self.__deprecate_argument(function)
deprecate = Deprecator()
deprecate_soon = Deprecator(pending=True)