How to warn about class (name) deprecation

后端 未结 7 1463
滥情空心
滥情空心 2020-12-13 12:21

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 de

相关标签:
7条回答
  • 2020-12-13 12:42

    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 and issubclass 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
    
    0 讨论(0)
  • 2020-12-13 12:44

    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)
    
    0 讨论(0)
  • 2020-12-13 12:51

    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>
    
    0 讨论(0)
  • 2020-12-13 12:52

    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.

    0 讨论(0)
  • 2020-12-13 13:04

    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)
    
    0 讨论(0)
  • 2020-12-13 13:06

    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.

    0 讨论(0)
提交回复
热议问题