Question

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

Je pense que pour assurer la rétrocompatibilité, il suffira d'utiliser un alias comme celui-ci :

class NewClsName:
    pass

OldClsName = NewClsName

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

Cependant, je ne sais pas comment fonctionnent les avertissements de dépréciation de la bibliothèque standard Python.J'imagine qu'il peut y avoir une belle magie pour gérer la dépréciation, par ex.permettant de le traiter comme des erreurs ou de le faire taire en fonction de l'option de ligne de commande de certains interprètes.

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

MODIFIER:L'approche fonction ne fonctionne pas pour moi (je l'ai déjà essayé) car la classe possède des méthodes de classe (méthodes d'usine) qui ne peuvent pas être appelées lorsque le 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( ... )

À cause 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 en introduisant des dérivations inutiles.En plus, OldClsName is not NewClsName ce qui n'est pas un problème dans la plupart des cas mais peut l'être en cas de code mal écrit utilisant la bibliothèque.

Je pourrais aussi créer un mannequin, sans rapport OldClsName class et implémenter un constructeur ainsi que des wrappers pour toutes les méthodes de classe qu'il contient, mais c'est une solution encore pire, à mon avis.

Était-ce utile?

La solution

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

Ouais, je pense que c'est une pratique assez courante :

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 éléments qui sous-classent de OldClsName - alors nous devons être intelligents.Si vous avez juste besoin de conserver l’accès aux méthodes de classe, ceci 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 une 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.

Autres conseils

Veuillez jeter un oeil à warnings.warn.

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

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

Pourquoi ne pas simplement sous-classer ?De cette façon, aucun code utilisateur ne devrait être cassé.

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

Voici la liste des exigences auxquelles une solution doit satisfaire :

  • L'instanciation d'une classe obsolète devrait déclencher un avertissement
  • Le sous-classement d'une classe obsolète devrait déclencher un avertissement
  • Soutien isinstance et issubclass chèques

Solution

Ceci 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 dont elle est une métaclasse, mais également pour chaque sous-classe de cette classe.Cela donne l'occasion de garantir qu'aucun cas de DeprecatedClass sera jamais instancié ou sous-classé.

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 de classes de base et doit remplacer les instances de DeprecatedClass avec NewClass.

Finalement, le isinstance et issubclass les contrôles sont effectués via __instancecheck__ et __subclasscheck__ défini dans 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

En python >= 3.6, vous pouvez facilement gérer les avertissements lors du 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.

Utiliser inspect module pour ajouter un espace réservé pour OldClass, alors OldClsName is NewClsName le contrôle réussira et un linter comme pylint l'informera comme une erreur.

obsolète.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 cours 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>

Depuis Python 3.7, vous pouvez fournir une personnalisation de l'accès aux attributs du module en utilisant __getattr__ (et __dir__).Tout est expliqué dans PPE 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, affichant le fichier source et le numéro de ligne de l'appelant (avec stacklevel=2).

Dans le code utilisateur, on pourrait 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__.__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()

Lorsque l'utilisateur exécute son script your_lib_usage.py, cela donnera 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

Note: 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
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top