web-dev-qa-db-fra.com

Comment avertir de la dépréciation d'une classe (nom)

J'ai renommé une classe python qui fait partie d'une bibliothèque. Je suis prêt à laisser la possibilité d'utiliser son nom précédent pendant un certain temps, mais je voudrais avertir l'utilisateur qu'il est obsolète et sera supprimé A l'avenir.

Je pense que pour assurer la compatibilité descendante, il suffira d'utiliser un alias comme ça:

class NewClsName:
    pass

OldClsName = NewClsName

Je n'ai aucune idée de comment marquer le OldClsName comme obsolète d'une manière élégante. Je pourrais peut-être faire de OldClsName une fonction qui émet un avertissement (aux journaux) et construit l'objet NewClsName à partir de ses paramètres (en utilisant *args et **kvargs) mais il ne semble pas assez élégant (ou peut-être que c'est?).

Cependant, je ne sais pas comment fonctionnent les Python avertissements de dépréciation de bibliothèque standard. J'imagine qu'il peut y avoir de la magie de Nice pour faire face à la dépréciation, par exemple en permettant de la traiter comme des erreurs ou de réduire au silence en fonction de certains interprètes). option de ligne de commande.

La question est: comment avertir les utilisateurs de l'utilisation d'un alias de classe obsolète (ou d'une classe obsolète en général).

EDIT : L'approche fonction ne fonctionne pas pour moi (je l'ai déjà essayé) parce que la classe a des méthodes de classe (méthodes d'usine) qui peuvent ' t être appelé lorsque OldClsName est défini comme une fonction. Le code suivant ne fonctionnera pas:

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

En raison de:

AttributeError: 'function' object has no attribute 'CreateVariant1'

L'héritage est-il ma seule option? Pour être honnête, cela ne me semble pas très clair - cela affecte la hiérarchie des classes par l'introduction d'une dérivation inutile. Aditionellement, OldClsName is not NewClsName ce qui n'est pas un problème dans la plupart des cas mais qui peut être un problème en cas de code mal écrit utilisant la bibliothèque.

Je pourrais également créer une classe OldClsName factice et indépendante et implémenter un constructeur ainsi que des wrappers pour toutes les méthodes de classe, mais c'est encore pire, à mon avis.

48
Dariusz Walczak

Peut-être que je pourrais faire d'OldClsName une fonction qui émet un avertissement (aux journaux) et construit l'objet NewClsName à partir de ses paramètres (en utilisant * args et ** kvargs) mais cela ne semble pas assez élégant (ou peut-être que c'est?).

Oui, je pense que c'est une pratique assez standard:

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

La seule chose délicate est que si vous avez des choses qui appartiennent à la sous-classe de OldClsName - alors nous devons être intelligents. Si vous avez juste besoin de conserver l'accès aux méthodes de classe, cela devrait le faire:

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)

Je ne l'ai pas testé, mais cela devrait vous donner l'idée - __call__ gérera la route d'instanciation normale, __getattr__ capturera les accès aux méthodes de classe et générera toujours l'avertissement, sans perturber la hiérarchie de votre classe.

32
AdamKG

Veuillez consulter warnings.warn .

Comme vous le verrez, l'exemple de la documentation est un avertissement de dépréciation:

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

Pourquoi tu ne sous-classe pas? De cette façon, aucun code utilisateur ne doit être rompu.

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

Voici la liste des exigences qu'une solution doit satisfaire:

  • L'instanciation d'une classe obsolète devrait déclencher un avertissement
  • Le sous-classement d'une classe déconseillée devrait déclencher un avertissement
  • Prise en charge des contrôles isinstance et issubclass

Solution

Cela peut être réalisé avec une métaclasse personnalisée:

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

Explication

DeprecatedClassMeta.__new__ La méthode est appelée non seulement pour une classe, c'est une métaclasse de mais aussi pour chaque sous-classe de cette classe. Cela permet de garantir qu'aucune instance de DeprecatedClass ne sera jamais instanciée ou sous-classée.

L'instanciation est simple. La métaclasse remplace la __new__ méthode de DeprecatedClass pour toujours renvoyer une instance de NewClass.

Le sous-classement n'est pas beaucoup plus difficile. DeprecatedClassMeta.__new__ reçoit une liste des classes de base et doit remplacer les instances de DeprecatedClass par NewClass.

Enfin, les vérifications isinstance et issubclass sont implémentées via __instancecheck__ et __subclasscheck__ défini dans PEP 3119 .


Tester

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
2
Kentzo

Dans python> = 3.6, vous pouvez facilement gérer les avertissements sur le sous-classement:

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

Surcharge __new__ devrait vous permettre d'avertir lorsque l'ancien constructeur de classe est appelé directement, mais je n'ai pas testé cela car je n'en ai pas besoin pour le moment.

2
Bitdancer

Utilisez le module inspect pour ajouter un espace réservé pour OldClass, puis OldClsName is NewClsName la vérification passera, et un linter comme pylint l'informera comme une erreur.

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

puis exécutez 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>
1
Nate Scarlet

Depuis Python 3.7, vous pouvez fournir une personnalisation de l'accès aux attributs de module en utilisant __getattr__ (et __dir__). Tout est expliqué dans PEP 562 . Dans l'exemple ci-dessous, j'ai implémenté __getattr__ et __dir__ afin de déprécier le "OldClsName" au profit de "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])

Dans le __getattr__ fonction, si une classe ou un nom de fonction obsolète est trouvé, un message d'avertissement est émis, indiquant le fichier source et le numéro de ligne de l'appelant (avec stacklevel=2).

Dans le code utilisateur, nous pourrions avoir:

# 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__.__+ " is created in use_new_class")


def use_old_class():
    obj = OldClsName.create_variant1()
    print(obj.__class__.__+ " is created in use_old_class")


if __== '__main__':
    use_new_class()
    use_old_class()

Lorsque l'utilisateur exécute son script your_lib_usage.py, il obtiendra quelque chose comme ceci:

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

Remarque: la trace de la pile est généralement écrite en STDERR.

Pour voir les avertissements d'erreur, vous devrez peut-être ajouter un indicateur "-W" dans la ligne de commande Python, par exemple:

python -W always your_lib_usage.py
0
Laurent LAPORTE