Como documentar pequenas alterações em funções complexas da API?

8

Digamos que temos uma função API complexa, importada de alguma biblioteca.

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

Quero escrever um invólucro simples em torno dessa função para fazer uma pequena alteração. Por exemplo , deve ser possível passar o primeiro argumento como uma string. Como documentar isso? Eu considerei as seguintes opções:

Opção 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)

Desvantagem: o usuário precisa consultar os documentos complex_api_functionpara obter informações sobre *argse **kwargs. Precisa de ajuste quando a cópia colou as seções da complex_api_functionalteração.

Opção 2:

Copie e cole complex_api_functiona assinatura (em vez de usar *argse **kwargs) e sua documentação. Faça uma pequena alteração na string de documento que menciona que o primeiro argumento também pode ser uma string. Adicione um exemplo.

Desvantagem: detalhada, deve ser alterada quando houver complex_api_functionalterações.

Opção 3:

Decore my_complex_api_functioncom functools.wraps(complex_api_function).

Desvantagem: não há informações que numbertambém possam ser uma sequência.


Estou procurando uma resposta que não se baseie nos detalhes do que muda my_complex_api_function. O procedimento deve funcionar para qualquer pequeno ajuste no original complex_api_function.

actual_panda
fonte

Respostas:

3

Eu recomendaria algo como o seguinte:

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
    """

Isso é claro e conciso. Mas lembre-se também de que, se estiver usando um sistema de documentação como o sphinx, vincule as funções com :ref:`bob`ou similar.

Legorooj
fonte
1
Eu nem mencionaria o tipo que complex_api_functionespera para seu parâmetro, pois isso apenas duplica as informações (talvez elas também tenham várias opções). Presumivelmente, o usuário do wrapper já está familiarizado com a função original e, caso contrário, você pode sempre apontá-los para os documentos originais. De qualquer forma, acho que esse é o caminho a seguir, apenas documente o que é adicionado à função original + fornecendo detalhes sobre como esse novo tipo é convertido no original (esses detalhes podem ser importantes). Ou seja, como esse argumento é tratado para ser compatível com a função original.
a_guest 31/03
1
Esse é um bom ponto para vincular - adicionei uma edição para a :ref:na docstring. No entanto, para pequenas alterações na API - como o OP está perguntando -, permite que os usuários comparem as funções de maneira mais simples. Nesse caso, o esforço mínimo pode dar um pouco mais de ganho aos usuários finais - e quando eu leio documentos, eu levo um documento de 12 páginas sobre um documento de 6 páginas na maioria dos casos, porque é um pouco mais fácil de entender.
Legorooj 01/04
5

Você pode automatizar a "especialização" da docstring original com um adendo . Por exemplo, pydoc está usando o atributo especial__doc__ . Você pode escrever um decorador que substitua automaticamente a função original __doc__pelo seu adendo.

Por exemplo:

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)

ou...

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)

Se você executar o pydoc ( pydoc3 -w my_module.py), ele produzirá: visualização do html gerado pelo pydoc

Nota adicional: Se você estiver usando o Python 3, poderá usar anotações para documentar o (s) tipo (s) dos seus parâmetros de função. Oferece muitos benefícios, não apenas documentação. Por exemplo:

from typing import Union

def my_complex_api_function(number_or_str: Union[int, float, str], *args, **kwargs):
Raphael Medaer
fonte
1
Isso tem a desvantagem de que as informações (novas) importantes são "ocultas" no final da cadeia de documentos (presumivelmente muito longa). Portanto, a capacidade de descoberta do novo recurso é muito baixa, enquanto é a única informação valiosa que é adicionada à cadeia de documentos existente. Além disso, entra em conflito com as declarações de tipo na sequência de documentos original. Ou seja, se o usuário observar a : floata sequência de documentos estendida, verá no topo e nunca chegará à conclusão de que também pode usar um straqui. Somente se, por acaso, rolarem até o final dos documentos, eles a descobrirão.
a_guest 31/03
1
Você pode adicionar o adendo no início e não no final ... Como uma nota de "revisão" no início de um documento.
Raphael Medaer
1
Outra questão é a duplicação (+ congelamento) de informações. Digamos que você construa um pacote que envie esse wrapper e especifique suas dependências como complex_package >= 1.1.0. Agora, quando você cria seu pacote, você precisa usar uma versão específica complex_package. Digamos que já exista complex_package==1.5.0no pypi e eles adicionaram um novo argumento de palavra-chave complex_api_functionna versão 1.3.0. De qualquer forma (usando 1.1.0ou 1.5.0), você terá informações desatualizadas / incorretas para um subgrupo de usuários no documento. O mesmo se aplica a mudanças futuras que ainda não são públicas.
a_guest 31/03
-1

Não tenho certeza se é isso que você está procurando, mas ajuda a evitar a pergunta completamente.

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
fonte
Obrigado. wrapped_api_funcnão possui docstring, portanto, o problema da documentação não foi resolvido.
actual_panda