Cómo advertir sobre la desaprobación de la clase (nombre)

He cambiado el nombre de una clase de python que forma parte de una biblioteca. Estoy dispuesto a dejar la posibilidad de usar su nombre anterior por algún tiempo, pero me gustaría advertir al usuario que está en desuso y que se eliminará en el futuro.

Creo que para proporcionar compatibilidad hacia atrás será suficiente usar un alias como ese:

class NewClsName: pass OldClsName = NewClsName 

No tengo idea de cómo marcar el OldClsName como obsoleto de una manera elegante. Tal vez podría hacer de OldClsName una función que emita una advertencia (a los registros) y construya el objeto NewClsName partir de sus parámetros (usando *args y **kvargs ) pero no parece lo suficientemente elegante (¿o quizás lo es?).

Sin embargo, no sé cómo funcionan las advertencias de desaprobación de la biblioteca estándar de Python. Me imagino que puede haber alguna magia agradable para lidiar con la desaprobación, por ejemplo, permitir el tratamiento de errores o silenciarlo dependiendo de la opción de línea de comando de algún intérprete.

La pregunta es: Cómo advertir a los usuarios sobre el uso de un alias de clase obsoleto (o una clase obsoleta en general).

    EDITAR : El enfoque de la función no me funciona (ya lo probé) porque la clase tiene algunos métodos de clase (métodos de fábrica) que no se pueden llamar cuando OldClsName se define como una función. El siguiente código no funcionará:

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

    Porque:

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

    ¿Es la herencia mi única opción? Para ser honesto, no me parece muy limpio, afecta a la jerarquía de clases mediante la introducción de derivaciones innecesarias. Además, OldClsName is not NewClsName lo que no es un problema en la mayoría de los casos, pero puede ser un problema en caso de un código mal escrito utilizando la biblioteca.

    También podría crear una clase OldClsName ficticia, no OldClsName e implementar un constructor, así como envoltorios para todos los métodos de clase en ella, pero es una solución aún peor, en mi opinión.

    Tal vez podría hacer de OldClsName una función que emita una advertencia (a los registros) y construya el objeto NewClsName a partir de sus parámetros (usando * args y ** kvargs) pero no parece lo suficientemente elegante (¿o quizás lo es?).

    Sí, creo que es una práctica bastante estándar:

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

    Lo único difícil es si tiene cosas que subclase de OldClsName ; entonces tenemos que ser inteligentes. Si solo necesitas mantener el acceso a los métodos de clase, esto debería hacerlo:

     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) 

    No lo he probado, pero eso debería darte una idea: __call__ manejará la ruta de __getattr__ normales, __getattr__ capturará los accesos a los métodos de clase y seguirá generando la advertencia, sin alterar el orden con tu jerarquía de clases.

    Por favor, eche un vistazo a las warnings.warn .

    Como verás, el ejemplo en la documentación es una advertencia de desaprobación:

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

    ¿Por qué no solo subclases? De esta manera no se debe romper ningún código de usuario.

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

    Aquí está la lista de requisitos que una solución debe satisfacer:

    • La instanciación de una clase en desuso debería generar una advertencia.
    • La subclasificación de una clase en desuso debería generar una advertencia
    • Soporta isinstance y issubclass cheques

    Solución

    Esto se puede lograr con una metaclase personalizada:

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

    Explicación

    DeprecatedClassMeta.__new__ método DeprecatedClassMeta.__new__ no solo para una clase de la que es una metaclase, sino también para cada subclase de esta clase. Esto brinda la oportunidad de garantizar que ninguna instancia de DeprecatedClass se ejemplificará o subclasificará.

    La instanciación es simple. La metaclase anula el método __new__ de DeprecatedClass para devolver siempre una instancia de NewClass .

    Subclasificar no es mucho más difícil. DeprecatedClassMeta.__new__ recibe una lista de clases básicas y necesita reemplazar las instancias de DeprecatedClass con NewClass .

    Finalmente, las isinstance y issubclass se implementan a través de __instancecheck__ y __subclasscheck__ definidas en PEP 3119 .


    Prueba

     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 

    Use el módulo de inspect para agregar el marcador de posición para OldClass , luego OldClsName is NewClsName verificación OldClsName is NewClsName se aprobará, y un indicador como pylint lo informará como 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()) 

    luego ejecuta 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> 

    En Python> = 3.6 puedes manejar fácilmente las advertencias en subclases:

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

    Sobrecargar __new__ debería permitirle advertir cuando se llama directamente al antiguo constructor de clases, pero no lo he probado ya que no lo necesito ahora.

    Desde Python 3.7, puede proporcionar una personalización del acceso a los atributos del módulo utilizando __getattr__ (y __dir__ ). Todo se explica en PEP 562 . En el siguiente ejemplo, implementé __getattr__ y __dir__ para dejar de usar “OldClsName” a favor 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]) 

    En la función __getattr__ , si se encuentra una clase en desuso o un nombre de función, se emite un mensaje de advertencia que muestra el archivo de origen y el número de línea de la persona que llama (con nivel de stacklevel=2 ).

    En el código de usuario, podríamos tener:

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

    Cuando el usuario ejecute su script your_lib_usage.py , obtendrá algo como esto:

     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 

    Nota: el seguimiento de la stack generalmente se escribe en STDERR.

    Para ver las advertencias de error, es posible que deba agregar un indicador “-W” en la línea de comandos de Python, por ejemplo:

     python -W always your_lib_usage.py