python: forma correcta de desaprobar el alias de parámetros en un clic

Quiero desaprobar un alias de parámetro al hacer clic (por ejemplo, cambiar de guiones bajos a guiones). Por un tiempo, quiero que ambas formulaciones sean válidas, pero lance un FutureWarning cuando el parámetro se invoca con el alias en desuso. Sin embargo, no he encontrado una forma de acceder al alias real con el que se invocó un parámetro.

En definitiva, quiero:

click.command()
click.option('--old', '--new')
def cli(*args, **kwargs):
    ...

para emitir una advertencia cuando la opción se invoca con –old, pero no cuando se invoca con –new. ¿Hay una forma limpia de hacer esto que no dependa demasiado del comportamiento indocumentado?

Intenté agregar una devolución de llamada a la opción click.option, pero parece que se llama después de que se analiza la opción, y los argumentos no contienen información sobre qué alias se usó realmente. Una solución probablemente sobrecargaría el clic.Opción o incluso el clic.Comando, pero no sé dónde tiene lugar el análisis real.

Mejor respuesta
Para poder saber qué nombre de opción se usó para seleccionar una opción en particular, le sugiero que aplique un parche al analizador de opciones utilizando algunas clases personalizadas. Esta solución termina heredándose de click.Option y click.Command:

Código:

import click
import warnings

class DeprecatedOption(click.Option):

    def __init__(self, *args, **kwargs):
        self.deprecated = kwargs.pop('deprecated', ())
        self.preferred = kwargs.pop('preferred', args[0][-1])
        super(DeprecatedOption, self).__init__(*args, **kwargs)

class DeprecatedOptionsCommand(click.Command):

    def make_parser(self, ctx):
        """Hook 'make_parser' and during processing check the name
            used to invoke the option to see if it is preferred"""

        parser = super(DeprecatedOptionsCommand, self).make_parser(ctx)

        # get the parser options
        options = set(parser._short_opt.values())
        options |= set(parser._long_opt.values())

        for option in options:
            if not isinstance(option.obj, DeprecatedOption):
                continue

            def make_process(an_option):
                """ Construct a closure to the parser option processor """

                orig_process = an_option.process
                deprecated = getattr(an_option.obj, 'deprecated', None)
                preferred = getattr(an_option.obj, 'preferred', None)
                msg = "Expected `deprecated` value for `{}`"
                assert deprecated is not None, msg.format(an_option.obj.name)

                def process(value, state):
                    """The function above us on the stack used 'opt' to
                        pick option from a dict, see if it is deprecated """

                    # reach up the stack and get 'opt'
                    import inspect
                    frame = inspect.currentframe()
                    try:
                        opt = frame.f_back.f_locals.get('opt')
                    finally:
                        del frame

                    if opt in deprecated:
                        msg = "'{}' has been deprecated, use '{}'"
                        warnings.warn(msg.format(opt, preferred),
                                      FutureWarning)

                    return orig_process(value, state)

                return process

            option.process = make_process(option)

        return parser

Usando las clases personalizadas:

Primero agregue un parámetro cls a @ click.command como:

@click.command(cls=DeprecatedOptionsCommand)

Luego, para cada opción que tenga valores en desuso, agregue los cls y los valores en desuso como:

@click.option('--old1', '--new1', cls=DeprecatedOption, deprecated=['--old1'])

Y opcionalmente puede agregar un valor preferido como:

@click.option('--old2', '-x', '--new2', cls=DeprecatedOption,
              deprecated=['--old2'], preferred='-x')

¿Como funciona esto?

Hay dos clases personalizadas aquí, derivan de dos clases de clic. Un click.Command personalizado y click.Opción.
Esto funciona porque hacer clic es un marco OO bien diseñado. El decorador @ click.command () generalmente crea una instancia de click.Command object, pero permite que este comportamiento se anule con el parámetro cls. @ click.option () funciona de manera similar. Por lo tanto, es relativamente fácil heredar de click.Command y click.Option en nuestras propias clases y sobrepasar los métodos deseados.

En el caso de la opción personalizada de clic.Option: DeprecatedOption, agregamos dos nuevos atributos de palabras clave: obsoletos y preferidos. en desuso es obligatorio y es una lista de nombres de comandos sobre los que se advertirá. preferido es opcional, y especifica el nombre del comando recomendado. Es una cadena y se establecerá de forma predeterminada el último nombre de comando en la línea de opción.

En el caso del clic personalizado. Comando: DeprecatedOptionsCommand, reemplazamos el método make_parser (). Esto nos permite parchear las instancias del analizador de opciones en la instancia del analizador. El analizador no está realmente destinado a la expansión como Comando y Opción, por lo que tenemos que ser un poco más creativos.

En este caso, todo el procesamiento de opciones en el analizador pasa por el método process (). Aquí repasamos ese método, y en el método parchado buscamos un nivel en el marco de la pila para encontrar la variable opt, que es el nombre que se usa para encontrar la opción. Luego, si este valor está en la lista en desuso, emitimos la advertencia.

Este código llega a algunas estructuras privadas en el analizador, pero es poco probable que esto sea un problema. Este código del analizador fue modificado por última vez hace 4 años. Es poco probable que el código del analizador sufra revisiones significativas.

Código de prueba:

@click.command(cls=DeprecatedOptionsCommand)
@click.option('--old1', '--new1', cls=DeprecatedOption,
              deprecated=['--old1'])
@click.option('--old2', '-x', '--new2', cls=DeprecatedOption,
              deprecated=['--old2'], preferred='-x')
def cli(**kwargs):
    click.echo("{}".format(kwargs))

if __name__ == "__main__":
    commands = (
        '--old1 5',
        '--new1 6',
        '--old2 7',
        '--new2 8',
        '-x 9',
        '',
        '--help',
    )

    import sys, time

    time.sleep(1)
    print('Click Version: {}'.format(click.__version__))
    print('Python Version: {}'.format(sys.version))
    for cmd in commands:
        try:
            time.sleep(0.1)
            print('-----------')
            print('> ' + cmd)
            time.sleep(0.1)
            cli(cmd.split())

        except BaseException as exc:
            if str(exc) != '0' and \
                    not isinstance(exc, (click.ClickException, SystemExit)):
                raise

Resultados:

Click Version: 6.7
Python Version: 3.6.3 (v3.6.3:2c5fed8, Oct  3 2017, 18:11:49) [MSC v.1900 64 bit (AMD64)]
-----------
> --old1 5
{'new1': '5', 'new2': None}
C:/Users/stephen/Documents/src/testcode/test.py:71: FutureWarning: '--old1' has been deprecated, use '--new1'
  FutureWarning)
-----------
> --new1 6
{'new1': '6', 'new2': None}
-----------
> --old2 7
{'new2': '7', 'new1': None}
C:/Users/stephen/Documents/src/testcode/test.py:71: FutureWarning: '--old2' has been deprecated, use '-x'
  FutureWarning)
-----------
> --new2 8
{'new2': '8', 'new1': None}
-----------
> -x 9
{'new2': '9', 'new1': None}
-----------
> 
{'new1': None, 'new2': None}
-----------
> --help
Usage: test.py [OPTIONS]

Options:
  --old1, --new1 TEXT
  -x, --old2, --new2 TEXT
  --help                   Show this message and exit.

Por favor indique la dirección original:python: forma correcta de desaprobar el alias de parámetros en un clic - Código de registro