How to warn about class (name) deprecation
PythonClassBackwards CompatibilityDeprecation WarningPython Problem Overview
I have renamed a python class that is part of a library. I am willing to leave a possibility to use its previous name for some time but would like to warn user that it's deprecated and will be removed in the future.
I think that to provide backward compatibility it will be enough to use an alias like that:
class NewClsName:
pass
OldClsName = NewClsName
I have no idea how to mark the OldClsName
as deprecated in an elegant way. Maybe I could make OldClsName
a function which emits a warning (to logs) and constructs the NewClsName
object from its parameters (using *args
and **kvargs
) but it doesn't seem elegant enough (or maybe it is?).
However, I don't know how Python standard library deprecation warnings work. I imagine that there may be some nice magic to deal with deprecation, e.g. allowing treating it as errors or silencing depending on some interpreter's command line option.
The question is: How to warn users about using an obsolete class alias (or obsolete class in general).
EDIT: The function approach doesn't work for me (I already gave it a try) because the class has some class methods (factory methods) which can't be called when the OldClsName
is defined as a function. Following code won't work:
class NewClsName(object):
@classmethod
def CreateVariant1( cls, ... ):
pass
@classmethod
def CreateVariant2( cls, ... ):
pass
def OldClsName(*args, **kwargs):
warnings.warn("The 'OldClsName' class was renamed [...]",
DeprecationWarning )
return NewClsName(*args, **kwargs)
OldClsName.CreateVariant1( ... )
Because of:
AttributeError: 'function' object has no attribute 'CreateVariant1'
Is inheritance my only option? To be honest, it doesn't look very clean to me - it affects class hierarchy through introduction of unnecessary derivation. Additionally, OldClsName is not NewClsName
what is not an issue in most cases but may be a problem in case of poorly written code using the library.
I could also create a dummy, unrelated OldClsName
class and implement a constructor as well as wrappers for all class methods in it, but it is even worse solution, in my opinion.
Python Solutions
Solution 1 - Python
> Maybe I could make OldClsName a function which emits a warning (to > logs) and constructs the NewClsName object from its parameters (using > *args and **kvargs) but it doesn't seem elegant enough (or maybe it is?).
Yup, I think that's pretty standard practice:
def OldClsName(*args, **kwargs):
from warnings import warn
warn("get with the program!")
return NewClsName(*args, **kwargs)
The only tricky thing is if you have things that subclass from OldClsName
- then we have to get clever. If you just need to keep access to class methods, this should do it:
class DeprecationHelper(object):
def __init__(self, new_target):
self.new_target = new_target
def _warn(self):
from warnings import warn
warn("Get with the program!")
def __call__(self, *args, **kwargs):
self._warn()
return self.new_target(*args, **kwargs)
def __getattr__(self, attr):
self._warn()
return getattr(self.new_target, attr)
OldClsName = DeprecationHelper(NewClsName)
I haven't tested it, but that should give you the idea - __call__
will handle the normal-instantation route, __getattr__
will capture accesses to the class methods & still generate the warning, without messing with your class heirarchy.
Solution 2 - Python
Please have a look at warnings.warn
.
As you'll see, the example in the documentation is a deprecation warning:
def deprecation(message):
warnings.warn(message, DeprecationWarning, stacklevel=2)
Solution 3 - Python
Here is the list of requirements a solution should satisfy:
- Instantiation of a deprecated class should raise a warning
- Subclassing of a deprecated class should raise a warning
- Support
isinstance
andissubclass
checks
Solution
This can be achieved with a custom metaclass:
class DeprecatedClassMeta(type):
def __new__(cls, name, bases, classdict, *args, **kwargs):
alias = classdict.get('_DeprecatedClassMeta__alias')
if alias is not None:
def new(cls, *args, **kwargs):
alias = getattr(cls, '_DeprecatedClassMeta__alias')
if alias is not None:
warn("{} has been renamed to {}, the alias will be "
"removed in the future".format(cls.__name__,
alias.__name__), DeprecationWarning, stacklevel=2)
return alias(*args, **kwargs)
classdict['__new__'] = new
classdict['_DeprecatedClassMeta__alias'] = alias
fixed_bases = []
for b in bases:
alias = getattr(b, '_DeprecatedClassMeta__alias', None)
if alias is not None:
warn("{} has been renamed to {}, the alias will be "
"removed in the future".format(b.__name__,
alias.__name__), DeprecationWarning, stacklevel=2)
# Avoid duplicate base classes.
b = alias or b
if b not in fixed_bases:
fixed_bases.append(b)
fixed_bases = tuple(fixed_bases)
return super().__new__(cls, name, fixed_bases, classdict,
*args, **kwargs)
def __instancecheck__(cls, instance):
return any(cls.__subclasscheck__(c)
for c in {type(instance), instance.__class__})
def __subclasscheck__(cls, subclass):
if subclass is cls:
return True
else:
return issubclass(subclass, getattr(cls,
'_DeprecatedClassMeta__alias'))
Explanation
DeprecatedClassMeta.__new__
method is called not only for a class it is a metaclass of but also for every subclass of this class. That gives an opportunity to ensure that no instance of DeprecatedClass
will ever be instantiated or subclassed.
Instantiation is simple. The metaclass overrides the __new__
method of DeprecatedClass
to always return an instance of NewClass
.
Subclassing is not much harder. DeprecatedClassMeta.__new__
receives a list of base classes and needs to replace instances of DeprecatedClass
with NewClass
.
Finally, the isinstance
and issubclass
checks are implemented via __instancecheck__
and __subclasscheck__
defined in PEP 3119.
Test
class NewClass:
foo = 1
class NewClassSubclass(NewClass):
pass
class DeprecatedClass(metaclass=DeprecatedClassMeta):
_DeprecatedClassMeta__alias = NewClass
class DeprecatedClassSubclass(DeprecatedClass):
foo = 2
class DeprecatedClassSubSubclass(DeprecatedClassSubclass):
foo = 3
assert issubclass(DeprecatedClass, DeprecatedClass)
assert issubclass(DeprecatedClassSubclass, DeprecatedClass)
assert issubclass(DeprecatedClassSubSubclass, DeprecatedClass)
assert issubclass(NewClass, DeprecatedClass)
assert issubclass(NewClassSubclass, DeprecatedClass)
assert issubclass(DeprecatedClassSubclass, NewClass)
assert issubclass(DeprecatedClassSubSubclass, NewClass)
assert isinstance(DeprecatedClass(), DeprecatedClass)
assert isinstance(DeprecatedClassSubclass(), DeprecatedClass)
assert isinstance(DeprecatedClassSubSubclass(), DeprecatedClass)
assert isinstance(NewClass(), DeprecatedClass)
assert isinstance(NewClassSubclass(), DeprecatedClass)
assert isinstance(DeprecatedClassSubclass(), NewClass)
assert isinstance(DeprecatedClassSubSubclass(), NewClass)
assert NewClass().foo == 1
assert DeprecatedClass().foo == 1
assert DeprecatedClassSubclass().foo == 2
assert DeprecatedClassSubSubclass().foo == 3
Solution 4 - Python
In python >= 3.6 you can easily handle warning on subclassing:
class OldClassName(NewClassName):
def __init_subclass__(self):
warn("Class has been renamed NewClassName", DeprecationWarning, 2)
Overloading __new__
should allow you to warn when the old class constructor is called directly, but I haven't tested that since I don't need it right now.
Solution 5 - Python
Why don't you just sub-class? This way no user code should be broken.
class OldClsName(NewClsName):
def __init__(self, *args, **kwargs):
warnings.warn("The 'OldClsName' class was renamed [...]",
DeprecationWarning)
NewClsName.__init__(*args, **kwargs)
Solution 6 - Python
Since Python 3.7, you can provide a customization of module attribute access using __getattr__
(and __dir__
). Everything is explained in PEP 562.
In the bellow example, I implemented __getattr__
and __dir__
in order to deprecate the “OldClsName” in favor of “NewClsNam”:
# your_lib.py
import warnings
__all__ = ["NewClsName"]
DEPRECATED_NAMES = [('OldClsName', 'NewClsName')]
class NewClsName:
@classmethod
def create_variant1(cls):
return cls()
def __getattr__(name):
for old_name, new_name in DEPRECATED_NAMES:
if name == old_name:
warnings.warn(f"The '{old_name}' class or function is renamed '{new_name}'",
DeprecationWarning,
stacklevel=2)
return globals()[new_name]
raise AttributeError(f"module {__name__} has no attribute {name}")
def __dir__():
return sorted(__all__ + [names[0] for names in DEPRECATED_NAMES])
In the __getattr__
function, if a deprecated class or function name is found, a warning message is emitted, showing the source file and line number of the caller (with stacklevel=2
).
In the user code, we could have:
# your_lib_usage.py
from your_lib import NewClsName
from your_lib import OldClsName
def use_new_class():
obj = NewClsName.create_variant1()
print(obj.__class__.__name__ + " is created in use_new_class")
def use_old_class():
obj = OldClsName.create_variant1()
print(obj.__class__.__name__ + " is created in use_old_class")
if __name__ == '__main__':
use_new_class()
use_old_class()
When the user run his script your_lib_usage.py
, it will get something like this:
NewClsName is created in use_new_class
NewClsName is created in use_old_class
/path/to/your_lib_usage.py:3: DeprecationWarning: The 'OldClsName' class or function is renamed 'NewClsName'
from your_lib import OldClsName
Note: the stack trace is usually written in STDERR.
To see the error warnings, you may need to add a “-W” flag in the Python command line, for instance:
python -W always your_lib_usage.py
Solution 7 - Python
Use inspect
module to add placeholder for OldClass
, then OldClsName is NewClsName
check will pass, and a linter like pylint will inform this as error.
deprecate.py
import inspect
import warnings
from functools import wraps
def renamed(old_name):
"""Return decorator for renamed callable.
Args:
old_name (str): This name will still accessible,
but call it will result a warn.
Returns:
decorator: this will do the setting about `old_name`
in the caller's module namespace.
"""
def _wrap(obj):
assert callable(obj)
def _warn():
warnings.warn('Renamed: {} -> {}'
.format(old_name, obj.__name__),
DeprecationWarning, stacklevel=3)
def _wrap_with_warn(func, is_inspect):
@wraps(func)
def _func(*args, **kwargs):
if is_inspect:
# XXX: If use another name to call,
# you will not get the warning.
frame = inspect.currentframe().f_back
code = inspect.getframeinfo(frame).code_context
if [line for line in code
if old_name in line]:
_warn()
else:
_warn()
return func(*args, **kwargs)
return _func
# Make old name available.
frame = inspect.currentframe().f_back
assert old_name not in frame.f_globals, (
'Name already in use.', old_name)
if inspect.isclass(obj):
obj.__init__ = _wrap_with_warn(obj.__init__, True)
placeholder = obj
else:
placeholder = _wrap_with_warn(obj, False)
frame.f_globals[old_name] = placeholder
return obj
return _wrap
test.py
from __future__ import print_function
from deprecate import renamed
@renamed('test1_old')
def test1():
return 'test1'
@renamed('Test2_old')
class Test2(object):
pass
def __init__(self):
self.data = 'test2_data'
def method(self):
return self.data
# pylint: disable=undefined-variable
# If not use this inline pylint option,
# there will be E0602 for each old name.
assert(test1() == test1_old())
assert(Test2_old is Test2)
print('# Call new name')
print(Test2())
print('# Call old name')
print(Test2_old())
then run python -W all test.py
:
test.py:22: DeprecationWarning: Renamed: test1_old -> test1
# Call new name
<__main__.Test2 object at 0x0000000007A147B8>
# Call old name
test.py:27: DeprecationWarning: Renamed: Test2_old -> Test2
<__main__.Test2 object at 0x0000000007A147B8>