2012-01-25 15 views
34

He cambiado el nombre de una clase python que forma 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.Cómo advertir acerca de la desactivación de clase (nombre)

creo que para proporcionar compatibilidad con versiones anteriores, será suficiente para utilizar un alias de esa manera:

class NewClsName: 
    pass 

OldClsName = NewClsName 

no tengo idea de cómo marcar la OldClsName como obsoletos de una manera elegante. Tal vez podría hacer OldClsName una función que emite una advertencia (a registros) y construye el objeto NewClsName desde sus parámetros (usando *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. Me imagino que puede haber algo de magia agradable para lidiar con la desaprobación, p. permitiendo tratarlo como errores o silenciar 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 la función no funciona para mí (yo ya lo intentó) debido a que la clase tiene algunos métodos de clase (métodos de fábrica) que no se puede llamar cuando el OldClsName se define como una función . 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(...) 

Debido:

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

es la herencia que 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 caso de código mal escrito que utiliza la biblioteca.

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

Respuesta

26

tal vez podría hacer OldClsName una función que emite una advertencia (a registros) y construye el objeto NewClsName desde sus parámetros (usando * args y ** kvargs) pero no parece lo suficientemente elegante (¿o tal vez lo es?).

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

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

La única cosa difícil es si tiene cosas esa subclase de OldClsName - entonces tenemos que conseguir inteligente.Si sólo necesita para mantener el acceso a los métodos de la clase, esto debe 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) 

Yo no lo he probado, pero que se debe dar la idea - __call__ se encargará de la ruta normal-instantation, __getattr__ capturará los accesos a los métodos de clase & aún generan la advertencia, sin interferir con la jerarquía de su clase.

+1

Apoyando la herencia debe ser fácil, así - acaba de escribir una clase 'OldClsName (NewClsName): # y sobrecarga __new__'. – delnan

+1

+1 para confirmar que se usa esta solución y para mencionar el módulo 'warnings'. Desafortunadamente, la solución de función no funciona para mí (ver la pregunta editada). Tal vez tienes algunas otras soluciones limpias? : D –

+0

Se actualizó con un ejemplo de uso de un objeto envoltorio que puede proxy de ambas llamadas y atributos de acceso a la nueva clase. – AdamKG

12

Por favor, eche un vistazo a warnings.warn.

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

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

¿Por qué no solo subclase? 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) 
+1

'isinstance()' comprueba ... – dAnjou

+0

'isinstance' también comprueba las subclases, por lo que esto no debería romper nada si esto es lo que trataste de insinuar. –

+0

las comprobaciones 'isinstance' fallarán. Considere, usted tiene el código heredado haciendo 'isinstance (obj, OldClsName)' y un nuevo código que ejemplifica 'obj = NewClsName()' luego 'isinstance (obj, OldClsName) == False' y su código se rompe: ¡vaya! – DylanYoung

0

módulo de uso inspect para añadir marcador de posición para OldClass, entonces OldClsName is NewClsName cheque pasará, y una desfibradora como pylint informará a esto 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()) 

continuación, ejecute python -W all test.py:

test.py:22: DeprecationWarning: Renamed: test1_old -> test1 
assert(test1() == test1_old()) 
# Call new name 
<__main__.Test2 object at 0x0000000007A147B8> 
# Call old name 
test.py:27: DeprecationWarning: Renamed: Test2_old -> Test2 
print(Test2_old()) 
<__main__.Test2 object at 0x0000000007A147B8> 
Cuestiones relacionadas