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>