¿Cómo documentar pequeños cambios en funciones API complejas?

8

Digamos que tenemos una función API compleja, importada de alguna biblioteca.

def complex_api_function(
        number, <lots of positional arguments>,
        <lots of keyword arguments>):
    '''really long docstring'''
    # lots of code

Quiero escribir un contenedor simple alrededor de esa función para hacer un pequeño cambio. Por ejemplo , debería ser posible pasar el primer argumento como una cadena. ¿Cómo documentar esto? Consideré las siguientes opciones:

Opción 1:

def my_complex_api_function(number_or_str, *args, **kwargs):
    '''
    Do something complex.

    Like `complex_api_function`, but first argument can be a string.

    Parameters
    ----------
    number_or_str : int or float or str
        Can be a number or a string that can be interpreted as a float.
        <copy paste description from complex_api_function docstring>
    *args
        Positional arguments passed to `complex_api_function`.
    **kwargs
        Keyword arguments passed to `complex_api_function`.

    Returns
    -------
    <copy paste from complex_api_function docstring>

    Examples
    --------
    <example where first argument is a string, e.g. '-5.0'>

    '''
    return complex_api_function(float(number_or_str), *args, **kwargs)

Desventaja: el usuario debe consultar los documentos de complex_api_functionpara obtener información sobre *argsy **kwargs. Necesita ajuste cuando la copia pegó secciones del complex_api_functioncambio.

Opcion 2:

Copie y pegue complex_api_functionla firma (en lugar de usar *argsy **kwargs) y su cadena de documentos. Realice un pequeño cambio en la cadena de documentación que menciona que el primer argumento también puede ser una cadena. Agrega un ejemplo.

Desventaja: detallado, debe cambiarse cuando complex_api_functioncambia.

Opcion 3:

Decorar my_complex_api_functioncon functools.wraps(complex_api_function).

Desventaja: no hay información que numbertambién pueda ser una cadena.


Estoy buscando una respuesta que no dependa de los detalles de los cambios my_complex_api_function. El procedimiento debería funcionar para cualquier pequeño ajuste al original complex_api_function.

actual_panda
fuente

Respuestas:

3

Recomiendo algo como lo siguiente:

def my_complex_api_function(number_or_str, *args, **kwargs):
    """This function is a light wrapper to `complex_api_function`.
    It allows you to pass a string or a number, whereas `complex_api_function` requires a 
    number. See :ref:`complex_api_function` for more details.

    :param number_or_str: number or str to convert to a number and pass to `complex_api_function`.
    :param args: Arguments to pass to `complex_api_function`
    :param kwargs: Keyword arguments to pass to `complex_api_function`
    :return: Output of `complex_api_function`, called with passed parameters
    """

Esto es claro y conciso. Pero recuerde también que, si utiliza un sistema de documentación como sphinx, para vincular las funciones con :ref:`bob`o similar.

Legorooj
fuente
1
Ni siquiera mencionaría qué tipo complex_api_functionespera para su parámetro, ya que solo duplica la información (tal vez también tienen múltiples opciones). Presumiblemente, el usuario del contenedor ya está familiarizado con la función original y, si no, siempre puede señalarlos a los documentos originales. De todos modos, creo que este es el camino a seguir, solo documente lo que se agrega a la función original + proporcionando detalles sobre cómo ese nuevo tipo se convierte al original (esos detalles pueden ser importantes). Es decir, cómo se trata ese argumento para ser compatible con la función original.
a_guest
1
Ese es un buen punto para vincular: he agregado una edición para un :ref:en la cadena de documentación. Sin embargo, para pequeños cambios en la API, como pregunta el OP, permite a los usuarios comparar las funciones de manera más simple. En este caso, el esfuerzo mínimo podría dar un poco más de ganancia a los usuarios finales, y cuando leo documentos, en la mayoría de los casos, tomo un documento de 12 páginas sobre un documento de 6 páginas, porque es un poco más fácil de entender.
Legorooj 01 de
5

Puede automatizar la "especialización" de la cadena de documentación original con un apéndice . Por ejemplo, pydoc está utilizando el atributo especial__doc__ . Podría escribir un decorador que anule automáticamente la función original __doc__con su anexo.

Por ejemplo:

def extend_docstring(original, addendum):
    def callable(func):
        func.__doc__ = original + addendum
        return func

    return callable


def complex_api_function(a, b, c):
    '''
    This is a very complex function.

    Parameters
    ----------
    a: int or float
        This is the argument A.
    b: ....
    '''
    print('do something')

@extend_docstring(
    complex_api_function.__doc__,
    '''
    Addendum
    --------
    Parameter a can also be a string
    '''
)
def my_complex_api_function(a, b, c):
    return complex_api_function(float(a), b, c)

o...

def extend_docstring(original):
    def callable(func):
        func.__doc__ = original + func.__doc__
        return func

    return callable


def complex_api_function(a, b, c):
    '''
    This is a very complex function.

    Parameters
    ----------
    a: int or float
        This is the argument A.
    b: ....
    '''
    print('do something')

@extend_docstring(complex_api_function.__doc__)
def my_complex_api_function(a, b, c):
    '''
    Addendum
    --------
    Parameter a can also be a string
    '''
    return complex_api_function(float(a), b, c)

Si ejecuta pydoc ( pydoc3 -w my_module.py) produce: vista previa de html generado por pydoc

Nota adicional: Si está utilizando Python 3, podría usar anotaciones para documentar el tipo (s) de los parámetros de su función. Ofrece muchos beneficios, no solo documentación. Por ejemplo:

from typing import Union

def my_complex_api_function(number_or_str: Union[int, float, str], *args, **kwargs):
Raphael Medaer
fuente
1
Esto tiene la desventaja de que la información importante (nueva) está "oculta" al final de la cadena de documentos (presumiblemente muy larga). Por lo tanto, la capacidad de detección de la nueva característica es muy baja, mientras que es la única información valiosa que se agrega a la cadena de documentos existente. Además, entra en conflicto las declaraciones de tipo en la cadena de documentación original. Es decir, si el usuario mira la cadena de documentos extendida, verá a : floaten la parte superior y nunca llegará a la conclusión de que también podría usar un straquí. Solo si por casualidad se desplazan hasta el final de los documentos, lo descubrirán.
a_guest
1
Puede agregar el apéndice al comienzo en lugar del final ... Como una nota de "revisión" al comienzo de un documento.
Raphael Medaer
1
Otro problema es la duplicación (+ congelación) de información. Supongamos que crea un paquete que incluye este contenedor y especifica sus dependencias como complex_package >= 1.1.0. Ahora, cuando crea su paquete, debe usar una versión específica para complex_package. Digamos que ya existe complex_package==1.5.0en pypi y agregaron un nuevo argumento de palabra clave complex_api_functionen la versión 1.3.0. De cualquier manera (usando 1.1.0o 1.5.0) tendrá información desactualizada / incorrecta para un subgrupo de sus usuarios en el documento. Lo mismo se aplica a los cambios futuros que aún no son públicos.
a_guest
-1

No estoy seguro de si esto es lo que está buscando, pero ayuda a evitar la pregunta por completo.

def first_as_num_or_str(func):
    '''Decorator allowing the first parameter of the given function to be a number or a string

    :param func: A function whose first argument is a number
    :return: `func`, but now the first argument is cast to a float
    ''' 
    def new_func(*args, **kwargs):
        func(float(args[0]), args[1:], kwargs)
    return new_func

wrapped_api_func = first_as_num_or_str(complex_api_function)
Ben Thayer
fuente
Gracias. wrapped_api_funcno tiene cadena de documentación, por lo que el problema de documentación no está resuelto.
actual_panda