如何警告类(名称)弃用

时间:2012-01-25 18:51:12

标签: python class backwards-compatibility deprecation-warning

我重命名了一个属于库的python类。我愿意在一段时间内保留使用其以前名称的可能性,但是我想警告用户它已被弃用,将来会被删除。

我认为为了提供向后兼容性,使用类似的别名就足够了:

class NewClsName:
    pass

OldClsName = NewClsName

我不知道如何以优雅的方式将OldClsName标记为已弃用。也许我可以使OldClsName函数发出警告(记录日志)并从其参数构建NewClsName对象(使用*args**kvargs)但它不会看起来很优雅(或者可能是?)。

但是,我不知道Python标准库弃用警告是如何工作的。我想可能有一些很好的魔术来处理弃用,例如允许将其视为错误或静默,具体取决于某些解释器的命令行选项。

问题是:如何警告用户使用过时的类别名(或一般的过时类)。

编辑:函数方法对我不起作用(我已经尝试过了)因为类有一些类方法(工厂方法),当{{ 1}}被定义为一个函数。以下代码无效:

OldClsName

因为:

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( ... )

继承是我唯一的选择吗?说实话,它对我来说看起来不太干净 - 它通过引入不必要的派生来影响类层次结构。另外,AttributeError: 'function' object has no attribute 'CreateVariant1' 在大多数情况下什么不是问题,但在使用该库的代码编写不好的情况下可能会出现问题。

我还可以创建一个虚拟的,不相关的OldClsName is not NewClsName类,并为其中的所有类方法实现构造函数和包装器,但在我看来,这是更糟糕的解决方案。

7 个答案:

答案 0 :(得分:31)

  

也许我可以让OldClsName成为发出警告的函数(to   log)并从其参数构造NewClsName对象(使用   * args和** kvargs)但它似乎不够优雅(或者它可能是?)。

是的,我认为这是非常标准的做法:

def OldClsName(*args, **kwargs):
    from warnings import warn
    warn("get with the program!")
    return NewClsName(*args, **kwargs)

唯一棘手的事情是如果你有来自OldClsName的子类 - 那么我们必须变得聪明。如果您只需要继续访问类方法,那么应该这样做:

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)

我还没有对它进行测试,但是这应该会给你一个想法 - __call__将处理正常的即时路由,__getattr__将捕获对类方法的访问权限。仍然会产生警告,而不会弄乱你的课程。

答案 1 :(得分:13)

请查看warnings.warn

正如您将看到的,文档中的示例是弃用警告:

def deprecation(message):
    warnings.warn(message, DeprecationWarning, stacklevel=2)

答案 2 :(得分:4)

为什么不是你的子课?这样就不会破坏用户代码。

class OldClsName(NewClsName):
    def __init__(self, *args, **kwargs):
        warnings.warn("The 'OldClsName' class was renamed [...]",
                      DeprecationWarning)
        NewClsName.__init__(*args, **kwargs)

答案 3 :(得分:2)

以下是解决方案应满足的要求列表:

  • 实例化已过时的类应发出警告
  • 不推荐使用的类的子类化应发出警告
  • 支持isinstanceissubclass检查

解决方案

这可以通过自定义元类实现:

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'))

说明

DeprecatedClassMeta.__new__方法不仅针对作为其元类的类而被调用,还针对该类的每个子类被调用。这样可以确保不会实例化或DeprecatedClass的任何实例。

说明很简单。元类覆盖DeprecatedClass的{​​{3}}方法,以始终返回NewClass的实例。

子类化并不困难。 DeprecatedClassMeta.__new__收到基类列表,需要用DeprecatedClass替换NewClass的实例。

最后,isinstanceissubclass检查是通过__new__中定义的__instancecheck____subclasscheck__实现的。


测试

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

答案 4 :(得分:1)

使用inspect模块为OldClass添加占位符,然后OldClsName is NewClsName检查将通过,并且像pylint这样的linter将通知此错误。

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())

然后运行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>

答案 5 :(得分:1)

在python> = 3.6中,您可以轻松处理有关子类的警告:

class OldClassName(NewClassName):
    def __init_subclass__(self):
        warn("Class has been renamed NewClassName", DeprecationWarning, 2)

重载__new__应该允许您在直接调用旧的类构造函数时发出警告,但是由于我现在不需要它,因此我尚未对其进行测试。

答案 6 :(得分:0)

从Python 3.7开始,您可以使用__getattr__(和__dir__)提供对模块属性访问的自定义。一切都在PEP 562中进行了说明。 在下面的示例中,我实现了__getattr____dir__以便弃用“ OldClsName”,而推荐使用“ 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])

__getattr__函数中,如果找到了不赞成使用的类或函数名称,则会发出警告消息,显示源文件和调用方的行号(带有stacklevel=2)。

在用户代码中,我们可以:

# 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()

当用户运行他的脚本your_lib_usage.py时,它将得到以下内容:

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

注意:堆栈跟踪通常是用STDERR编写的。

要查看错误警告,您可能需要在Python命令行中添加“ -W”标志,例如:

python -W always your_lib_usage.py