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.
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.
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)
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)
Voici la liste des exigences qu'une solution doit satisfaire:
isinstance
et issubclass
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'))
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 .
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
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.
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>
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