decoradores en la lib estándar de Python (@deprecated específicamente)

127

Necesito marcar las rutinas como obsoletas, pero aparentemente no hay un decorador de biblioteca estándar para la obsolescencia. Conozco las recetas y el módulo de advertencias, pero mi pregunta es: ¿por qué no hay un decorador de biblioteca estándar para esta tarea (común)?

Pregunta adicional: ¿hay decoradores estándar en la biblioteca estándar?

Stefano Borini
fuente
13
ahora hay un paquete de desaprobación
muon
11
Entiendo las formas de hacerlo, pero vine aquí para tener una idea de por qué no está en la biblioteca estándar (como supongo que es el caso del OP) y no veo una buena respuesta a la pregunta real
SwimBikeRun
44
¿Por qué sucede tan a menudo que las preguntas obtienen docenas de respuestas que ni siquiera intentan responder la pregunta e ignoran activamente cosas como "Estoy al tanto de las recetas"? ¡Es enloquecedor!
Catskul
1
@Catskul debido a puntos de internet falsos.
Stefano Borini
1
Puede usar la Biblioteca obsoleta .
Laurent LAPORTE

Respuestas:

59

Aquí hay un fragmento, modificado de los citados por Leandro:

import warnings
import functools

def deprecated(func):
    """This is a decorator which can be used to mark functions
    as deprecated. It will result in a warning being emitted
    when the function is used."""
    @functools.wraps(func)
    def new_func(*args, **kwargs):
        warnings.simplefilter('always', DeprecationWarning)  # turn off filter
        warnings.warn("Call to deprecated function {}.".format(func.__name__),
                      category=DeprecationWarning,
                      stacklevel=2)
        warnings.simplefilter('default', DeprecationWarning)  # reset filter
        return func(*args, **kwargs)
    return new_func

# Examples

@deprecated
def some_old_function(x, y):
    return x + y

class SomeClass:
    @deprecated
    def some_old_method(self, x, y):
        return x + y

Debido a que en algunos intérpretes la primera solución expuesta (sin manejo del filtro) puede provocar una supresión de advertencia.

Patrizio Bertoni
fuente
14
¿Por qué no usar en functools.wrapslugar de establecer el nombre y el documento de esa manera?
Maximiliano
1
@Maximilian: Editado para agregar eso, para evitar que los futuros copiadores de este código también lo hagan mal
Eric
17
No me gustan los efectos secundarios (activar / desactivar el filtro). No es el trabajo del decorador decidir esto.
Kentzo
1
Encender y apagar el filtro puede provocar bugs.python.org/issue29672
gerrit
44
no responde la pregunta real
Catskul
44

Aquí hay otra solución:

Este decorador (una fábrica de decoradores de hecho) le permite dar un mensaje de razón . También es más útil para ayudar al desarrollador a diagnosticar el problema al proporcionar el nombre de archivo de origen y el número de línea .

EDITAR : Este código utiliza la recomendación de Zero: reemplaza la warnings.warn_explicitlínea por warnings.warn(msg, category=DeprecationWarning, stacklevel=2), que imprime el sitio de llamada de función en lugar del sitio de definición de función. Facilita la depuración.

EDIT2 : esta versión permite al desarrollador especificar un mensaje opcional de "razón".

import functools
import inspect
import warnings

string_types = (type(b''), type(u''))


def deprecated(reason):
    """
    This is a decorator which can be used to mark functions
    as deprecated. It will result in a warning being emitted
    when the function is used.
    """

    if isinstance(reason, string_types):

        # The @deprecated is used with a 'reason'.
        #
        # .. code-block:: python
        #
        #    @deprecated("please, use another function")
        #    def old_function(x, y):
        #      pass

        def decorator(func1):

            if inspect.isclass(func1):
                fmt1 = "Call to deprecated class {name} ({reason})."
            else:
                fmt1 = "Call to deprecated function {name} ({reason})."

            @functools.wraps(func1)
            def new_func1(*args, **kwargs):
                warnings.simplefilter('always', DeprecationWarning)
                warnings.warn(
                    fmt1.format(name=func1.__name__, reason=reason),
                    category=DeprecationWarning,
                    stacklevel=2
                )
                warnings.simplefilter('default', DeprecationWarning)
                return func1(*args, **kwargs)

            return new_func1

        return decorator

    elif inspect.isclass(reason) or inspect.isfunction(reason):

        # The @deprecated is used without any 'reason'.
        #
        # .. code-block:: python
        #
        #    @deprecated
        #    def old_function(x, y):
        #      pass

        func2 = reason

        if inspect.isclass(func2):
            fmt2 = "Call to deprecated class {name}."
        else:
            fmt2 = "Call to deprecated function {name}."

        @functools.wraps(func2)
        def new_func2(*args, **kwargs):
            warnings.simplefilter('always', DeprecationWarning)
            warnings.warn(
                fmt2.format(name=func2.__name__),
                category=DeprecationWarning,
                stacklevel=2
            )
            warnings.simplefilter('default', DeprecationWarning)
            return func2(*args, **kwargs)

        return new_func2

    else:
        raise TypeError(repr(type(reason)))

Puede usar este decorador para funciones , métodos y clases .

Aquí hay un ejemplo simple:

@deprecated("use another function")
def some_old_function(x, y):
    return x + y


class SomeClass(object):
    @deprecated("use another method")
    def some_old_method(self, x, y):
        return x + y


@deprecated("use another class")
class SomeOldClass(object):
    pass


some_old_function(5, 3)
SomeClass().some_old_method(8, 9)
SomeOldClass()

Obtendrás:

deprecated_example.py:59: DeprecationWarning: Call to deprecated function or method some_old_function (use another function).
  some_old_function(5, 3)
deprecated_example.py:60: DeprecationWarning: Call to deprecated function or method some_old_method (use another method).
  SomeClass().some_old_method(8, 9)
deprecated_example.py:61: DeprecationWarning: Call to deprecated class SomeOldClass (use another class).
  SomeOldClass()

EDITAR3: este decorador ahora es parte de la biblioteca obsoleta:

Nueva versión estable v1.2.10 🎉

Laurent LAPORTE
fuente
66
Funciona bien: prefiero reemplazar la warn_explicitlínea con la warnings.warn(msg, category=DeprecationWarning, stacklevel=2)que imprime el sitio de llamada de función en lugar del sitio de definición de función. Facilita la depuración.
Cero
Hola, me gustaría utilizar su fragmento de código en una biblioteca con licencia GPLv3 . ¿Estaría dispuesto a volver a licenciar su código bajo GPLv3 o cualquier licencia más permisiva , para que yo pueda hacerlo legalmente?
gerrit
1
@LaurentLAPORTE lo sé. CC-BY-SO no permite el uso dentro de GPLv3 (debido al bit de compartir), por lo que le pregunto si estaría dispuesto a liberar este código específicamente adicionalmente bajo una licencia compatible con GPL. Si no, está bien, y no usaré tu código.
gerrit
2
no responde la pregunta real
Catskul
15

Como sugirió muon , puede instalar el deprecationpaquete para esto.

La deprecationbiblioteca proporciona un deprecateddecorador y un fail_if_not_removeddecorador para sus pruebas.

Instalación

pip install deprecation

Ejemplo de uso

import deprecation

@deprecation.deprecated(deprecated_in="1.0", removed_in="2.0",
                        current_version=__version__,
                        details="Use the bar function instead")
def foo():
    """Do some stuff"""
    return 1

Consulte http://deprecation.readthedocs.io/ para obtener la documentación completa.

Stevoisiak
fuente
44
no responde la pregunta real
Catskul
1
Tenga en cuenta que PyCharm no reconoce esto
cz
11

Supongo que la razón es que el código Python no puede procesarse estáticamente (como lo hizo para los compiladores de C ++), no puede recibir advertencias sobre el uso de algunas cosas antes de usarlo realmente. No creo que sea una buena idea enviar un correo no deseado al usuario de su script con un montón de mensajes "Advertencia: este desarrollador de este script está utilizando una API obsoleta".

Actualización: pero puedes crear un decorador que transformará la función original en otra. La nueva función marcará / comprobará el interruptor indicando que ya se llamó a esta función y mostrará un mensaje solo al encender el interruptor. Y / o al salir, puede imprimir una lista de todas las funciones obsoletas utilizadas en el programa.

ony
fuente
3
Y debería ser capaz de indicar desaprobación cuando la función se importa desde el módulo . Decorador sería una herramienta adecuada para eso.
Janusz Lenar
@ JanuszLenar, esa advertencia se mostrará incluso si no usamos esa función obsoleta. Pero supongo que puedo actualizar mi respuesta con alguna pista.
ony
8

Puedes crear un archivo utils

import warnings

def deprecated(message):
  def deprecated_decorator(func):
      def deprecated_func(*args, **kwargs):
          warnings.warn("{} is a deprecated function. {}".format(func.__name__, message),
                        category=DeprecationWarning,
                        stacklevel=2)
          warnings.simplefilter('default', DeprecationWarning)
          return func(*args, **kwargs)
      return deprecated_func
  return deprecated_decorator

Y luego importe el decorador de desuso de la siguiente manera:

from .utils import deprecated

@deprecated("Use method yyy instead")
def some_method()"
 pass
Erika Dsouza
fuente
¡Gracias, estoy usando esto para enviar al usuario al lugar correcto en lugar de solo mostrar el mensaje de desaprobación!
Attanasio alemán
3
no responde la pregunta real
Catskul
2

ACTUALIZACIÓN: Creo que es mejor, cuando mostramos DeprecationWarning solo la primera vez para cada línea de código y cuando podemos enviar algún mensaje:

import inspect
import traceback
import warnings
import functools

import time


def deprecated(message: str = ''):
    """
    This is a decorator which can be used to mark functions
    as deprecated. It will result in a warning being emitted
    when the function is used first time and filter is set for show DeprecationWarning.
    """
    def decorator_wrapper(func):
        @functools.wraps(func)
        def function_wrapper(*args, **kwargs):
            current_call_source = '|'.join(traceback.format_stack(inspect.currentframe()))
            if current_call_source not in function_wrapper.last_call_source:
                warnings.warn("Function {} is now deprecated! {}".format(func.__name__, message),
                              category=DeprecationWarning, stacklevel=2)
                function_wrapper.last_call_source.add(current_call_source)

            return func(*args, **kwargs)

        function_wrapper.last_call_source = set()

        return function_wrapper
    return decorator_wrapper


@deprecated('You must use my_func2!')
def my_func():
    time.sleep(.1)
    print('aaa')
    time.sleep(.1)


def my_func2():
    print('bbb')


warnings.simplefilter('always', DeprecationWarning)  # turn off filter
print('before cycle')
for i in range(5):
    my_func()
print('after cycle')
my_func()
my_func()
my_func()

Resultado:

before cycle
C:/Users/adr-0/OneDrive/Projects/Python/test/unit1.py:45: DeprecationWarning: Function my_func is now deprecated! You must use my_func2!
aaa
aaa
aaa
aaa
aaa
after cycle
C:/Users/adr-0/OneDrive/Projects/Python/test/unit1.py:47: DeprecationWarning: Function my_func is now deprecated! You must use my_func2!
aaa
C:/Users/adr-0/OneDrive/Projects/Python/test/unit1.py:48: DeprecationWarning: Function my_func is now deprecated! You must use my_func2!
aaa
C:/Users/adr-0/OneDrive/Projects/Python/test/unit1.py:49: DeprecationWarning: Function my_func is now deprecated! You must use my_func2!
aaa

Process finished with exit code 0

Simplemente podemos hacer clic en la ruta de advertencia e ir a la línea en PyCharm.

ADR
fuente
2
no responde la pregunta real
Catskul
0

Aumentando esta respuesta de Steven Vascellaro :

Si usa Anaconda, primero instale el deprecationpaquete:

conda install -c conda-forge deprecation 

Luego pegue lo siguiente en la parte superior del archivo

import deprecation

@deprecation.deprecated(deprecated_in="1.0", removed_in="2.0",
                    current_version=__version__,
                    details="Use the bar function instead")
def foo():
    """Do some stuff"""
    return 1

Consulte http://deprecation.readthedocs.io/ para obtener la documentación completa.

omerbp
fuente
44
no responde la pregunta real
Catskul