que developing python class backwards-compatibility deprecation-warning

developing - python download



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

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

Creo que para proporcionar compatibilidad con versiones anteriores bastará con 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 OldClsName una función que emite una advertencia (a registros) y construye el objeto NewClsName partir de sus parámetros (utilizando *args y **kvargs ), pero no parece lo suficientemente elegante (¿o tal vez lo es?).

Sin embargo, no sé cómo funcionan las advertencias de desaprobación de la biblioteca estándar de Python. Imagino que puede haber algo de magia para lidiar con la desaprobación, por ejemplo, permitir tratarlo como 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 clase obsoleta en general).

EDITAR : El enfoque de función no funciona para mí (ya lo intenté) 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 la jerarquía de clases mediante la introducción de una derivación innecesaria. 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 el caso de un código mal escrito que utiliza la biblioteca.

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


Tal vez podría hacer OldClsName una función que emite una advertencia (a registros) y construye el objeto NewClsName a partir de sus parámetros (utilizando * args y ** kvargs), pero no parece lo suficientemente elegante (¿o tal vez 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 delicado es que si tienes las cosas de la subclase de OldClsName , entonces tenemos que ser inteligentes. Si solo necesita 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 la idea: __call__ manejará la ruta de instantación normal, __getattr__ capturará los accesos a los métodos de clase y aún generará la advertencia, sin interferir con tu jerarquía de clases.


¿Por qué no solo sub-clase? De esta forma, ningún código de usuario debe romperse.

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


Por favor, eche un vistazo a warnings.warn .

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

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


Utilice el módulo de inspect para agregar un marcador de posición para OldClass , luego OldClsName is NewClsName comprobación de OldClsName is NewClsName , y un filtro tipo pylint informará esto como un 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>