Как предупредить об устаревании класса (имени) - PullRequest
41 голосов
/ 25 января 2012

Я переименовал класс Python, который является частью библиотеки. Я хочу оставить возможность использовать его прежнее имя в течение некоторого времени, но хотел бы предупредить пользователя, что оно устарело и будет удалено в будущем.

Я думаю, что для обеспечения обратной совместимости будет достаточно использовать такой псевдоним:

class NewClsName:
    pass

OldClsName = NewClsName

Я понятия не имею, как элегантно пометить OldClsName как устаревшее. Может быть, я мог бы сделать OldClsName функцией, которая выдает предупреждение (в журналы) и создает объект NewClsName из его параметров (используя *args и **kvargs), но он не выглядит достаточно элегантным (или, может быть, это ?).

Однако я не знаю, как работают предупреждения об устаревании стандартной библиотеки Python. Я предполагаю, что может быть какая-то хорошая магия, чтобы иметь дело с обесцениванием, например. позволяя рассматривать его как ошибки или молчание в зависимости от параметра командной строки какого-либо интерпретатора.

Вопрос: как предупредить пользователей об использовании устаревшего псевдонима класса (или устаревшего класса в целом).

РЕДАКТИРОВАТЬ : функциональный подход у меня не работает (я уже попробовал), потому что у класса есть некоторые методы класса (фабричные методы), которые нельзя вызвать, когда 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, что не является проблемой в большинстве случаев, но может быть проблемой в случае плохо написанного кода с использованием библиотеки.

Я мог бы также создать фиктивный, не связанный OldClsName класс и реализовать конструктор, а также обертки для всех методов класса в нем, но, на мой взгляд, это еще худшее решение.

Ответы [ 7 ]

31 голосов
/ 25 января 2012

Может быть, я мог бы сделать OldClsName функцией, которая выдает предупреждение (для журналов) и создает объект 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__ будет фиксировать доступ к методам класса и по-прежнему генерировать предупреждение, не вмешиваясь в иерархию классов.

13 голосов
/ 25 января 2012

Пожалуйста, посмотрите на warnings.warn.

Как вы увидите, примером в документации является предупреждение об устаревании:

def deprecation(message):
    warnings.warn(message, DeprecationWarning, stacklevel=2)
4 голосов
/ 01 октября 2014

Почему бы тебе просто не подкласс?Таким образом, код пользователя не должен быть взломан.

class OldClsName(NewClsName):
    def __init__(self, *args, **kwargs):
        warnings.warn("The 'OldClsName' class was renamed [...]",
                      DeprecationWarning)
        NewClsName.__init__(*args, **kwargs)
2 голосов
/ 23 ноября 2018

В python> = 3.6 вы можете легко обработать предупреждение о создании подкласса:

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

Перегрузка __new__ должна позволять вам предупреждать, когда старый конструктор класса вызывается напрямую, но я этого не проверял, поскольку он мне сейчас не нужен.

2 голосов
/ 30 августа 2018

Вот список требований, которым должно удовлетворять решение:

  • Создание устаревшего класса должно вызвать предупреждение
  • Подклассы устаревшего класса должны вызывать предупреждение
  • Поддержка isinstance и issubclass чеки

Решение

Этого можно достичь с помощью пользовательского метакласса:

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 никогда не будет создан или переклассифицирован.

Инстанциация проста. Метакласс переопределяет метод __new__ DeprecatedClass, чтобы всегда возвращать экземпляр NewClass.

Субклассирование не намного сложнее. DeprecatedClassMeta.__new__ получает список базовых классов и должен заменить экземпляры DeprecatedClass на NewClass.

Наконец, проверки isinstance и issubclass осуществляются через __instancecheck__ и __subclasscheck__, определенные в 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
1 голос
/ 26 февраля 2018

Используйте модуль inspect, чтобы добавить заполнитель для OldClass, затем проверка OldClsName is NewClsName пройдет, и столбец, похожий на линтер, сообщит об этом как об ошибке.

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>
0 голосов
/ 13 марта 2019

Начиная с 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.

Чтобы увидеть предупреждения об ошибках, вам может потребоваться добавить флаг «-W» в командной строке Python, например:

python -W always your_lib_usage.py
...