Simple tooling for marking deprecated functions or classes and re-routing to their successors.
The common use-case is moving your functions across a codebase or outsourcing some functionalities to new packages. For most of these cases, you want to maintain some compatibility, so you cannot simply remove the past function. You also want to warn users for some time that the functionality they have been using has moved and is now deprecated in favor of another function (which should be used instead) and will soon be removed completely.
Another good aspect is not overwhelming users with too many warnings, so per function/class, this warning is raised only N times in the preferred stream (warning, logger, etc.).
Simple installation from PyPI:
pip install pyDeprecateOther installations
Simply install with pip from source:
pip install https://github.com/Borda/pyDeprecate/archive/main.zipThe functionality is kept simple and all defaults should be reasonable, but you can still do extra customization such as:
- define user warning message and preferred stream
- extended argument mapping to target function/method
- define deprecation logic for self arguments
- specify warning count per:
- called function (for func deprecation)
- used arguments (for argument deprecation)
- define conditional skip (e.g. depending on some package version)
In particular the target values (cases):
- None - raise only warning message (ignore all argument mapping)
- True - deprecate some argument of itself (argument mapping should be specified)
- Callable - forward call to new methods (optionally also argument mapping or extras)
It is very straightforward: you forward your function call to a new function and all arguments are mapped:
def base_sum(a: int = 0, b: int = 3) -> int:
"""My new function anywhere in the codebase or even other package."""
return a + b
# ---------------------------
from deprecate import deprecated
@deprecated(target=base_sum, deprecated_in="0.1", remove_in="0.5")
def depr_sum(a: int, b: int = 5) -> int:
"""
My deprecated function which now has an empty body
as all calls are routed to the new function.
"""
pass # or you can just place docstring as one above
# calling this function will raise a deprecation warning:
# The `depr_sum` was deprecated since v0.1 in favor of `__main__.base_sum`.
# It will be removed in v0.5.
print(depr_sum(1, 2))sample output:
3
Another more complex example is using argument mapping is:
Advanced example
import logging
from sklearn.metrics import accuracy_score
from deprecate import deprecated, void
@deprecated(
# use standard sklearn accuracy implementation
target=accuracy_score,
# custom warning stream
stream=logging.warning,
# number of warnings per lifetime (with -1 for always)
num_warns=5,
# custom message template
template_mgs="`%(source_name)s` was deprecated, use `%(target_path)s`",
# as target args are different, define mapping from source to target func
args_mapping={"preds": "y_pred", "target": "y_true", "blabla": None},
)
def depr_accuracy(preds: list, target: list, blabla: float) -> float:
"""My deprecated function which is mapping to sklearn accuracy."""
# to stop complain your IDE about unused argument you can use void/empty function
return void(preds, target, blabla)
# calling this function will raise a deprecation warning:
# WARNING:root:`depr_accuracy` was deprecated, use `sklearn.metrics.accuracy_score`
print(depr_accuracy([1, 0, 1, 2], [0, 1, 1, 2], 1.23))sample output:
0.5
Base use-case with no forwarding and just raising a warning:
from deprecate import deprecated
@deprecated(target=None, deprecated_in="0.1", remove_in="0.5")
def my_sum(a: int, b: int = 5) -> int:
"""My deprecated function which still has to have implementation."""
return a + b
# calling this function will raise a deprecation warning:
# The `my_sum` was deprecated since v0.1. It will be removed in v0.5.
print(my_sum(1, 2))sample output:
3
We also support deprecation and argument mapping for the function itself:
from deprecate import deprecated
@deprecated(
# define as deprecation some self argument - mapping
target=True,
args_mapping={"coef": "new_coef"},
# common version info
deprecated_in="0.2",
remove_in="0.4",
)
def any_pow(base: float, coef: float = 0, new_coef: float = 0) -> float:
"""My function with deprecated argument `coef` mapped to `new_coef`."""
return base**new_coef
# calling this function will raise a deprecation warning:
# The `any_pow` uses deprecated arguments: `coef` -> `new_coef`.
# They were deprecated since v0.2 and will be removed in v0.4.
print(any_pow(2, 3))sample output:
8
Eventually you can set multiple deprecation levels via chaining deprecation arguments as each could be deprecated in another version:
Multiple deprecation levels
from deprecate import deprecated
@deprecated(
True,
deprecated_in="0.3",
remove_in="0.6",
args_mapping=dict(c1="nc1"),
template_mgs="Depr: v%(deprecated_in)s rm v%(remove_in)s for args: %(argument_map)s.",
)
@deprecated(
True,
deprecated_in="0.4",
remove_in="0.7",
args_mapping=dict(nc1="nc2"),
template_mgs="Depr: v%(deprecated_in)s rm v%(remove_in)s for args: %(argument_map)s.",
)
def any_pow(base, c1: float = 0, nc1: float = 0, nc2: float = 2) -> float:
return base**nc2
# calling this function will raise deprecation warnings:
# FutureWarning('Depr: v0.3 rm v0.6 for args: `c1` -> `nc1`.')
# FutureWarning('Depr: v0.4 rm v0.7 for args: `nc1` -> `nc2`.')
print(any_pow(2, 3))sample output:
8
Conditional skip of which can be used for mapping between different target functions depending on additional input such as package version
from deprecate import deprecated
FAKE_VERSION = 1
def version_greater_1():
return FAKE_VERSION > 1
@deprecated(True, "0.3", "0.6", args_mapping=dict(c1="nc1"), skip_if=version_greater_1)
def skip_pow(base, c1: float = 1, nc1: float = 1) -> float:
return base ** (c1 - nc1)
# calling this function will raise a deprecation warning
print(skip_pow(2, 3))
# change the fake versions
FAKE_VERSION = 2
# will not raise any warning
print(skip_pow(2, 3))sample output:
0.25
4
This can be beneficial with multiple deprecation levels shown above...
This case can be quite complex as you may deprecate just some methods, here we show full class deprecation:
class NewCls:
"""My new class anywhere in the codebase or other package."""
def __init__(self, c: float, d: str = "abc"):
self.my_c = c
self.my_d = d
# ---------------------------
from deprecate import deprecated, void
class PastCls(NewCls):
"""
The deprecated class shall be inherited from the successor class
to hold all methods.
"""
@deprecated(target=NewCls, deprecated_in="0.2", remove_in="0.4")
def __init__(self, c: int, d: str = "efg"):
"""
You place the decorator around __init__ as you want
to warn user just at the time of creating object.
"""
void(c, d)
# calling this function will raise a deprecation warning:
# The `PastCls` was deprecated since v0.2 in favor of `__main__.NewCls`.
# It will be removed in v0.4.
inst = PastCls(7)
print(inst.my_c) # returns: 7
print(inst.my_d) # returns: "efg"sample output:
7
efg
Have you faced this issue in the past or are you facing it now? Do you have good ideas for improvement? All contributions are welcome!