Wie dokumentiere ich kleine Änderungen an komplexen API-Funktionen?

8

Angenommen, wir haben eine komplexe API-Funktion, die aus einer Bibliothek importiert wurde.

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

Ich möchte einen einfachen Wrapper um diese Funktion schreiben, um eine winzige Änderung vorzunehmen. Beispielsweise sollte es möglich sein, das erste Argument als Zeichenfolge zu übergeben. Wie kann man das dokumentieren? Ich habe folgende Optionen in Betracht gezogen:

Option 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)

Nachteil: Der Benutzer muss sich die Dokumente von ansehen complex_api_function, um Informationen über *argsund zu erhalten **kwargs. Muss angepasst werden, wenn beim Kopieren Abschnitte aus der complex_api_functionÄnderung eingefügt werden .

Option 2:

Kopieren Sie die complex_api_functionSignatur (anstelle von *argsund **kwargs) und deren Dokumentzeichenfolge und fügen Sie sie ein. Nehmen Sie eine kleine Änderung an der Dokumentzeichenfolge vor, in der erwähnt wird, dass das erste Argument auch eine Zeichenfolge sein kann. Fügen Sie ein Beispiel hinzu.

Nachteil: ausführlich, muss bei complex_api_functionÄnderungen geändert werden .

Option 3:

Dekorieren Sie my_complex_api_functionmit functools.wraps(complex_api_function).

Nachteil: Es gibt keine Informationen, numberdie auch eine Zeichenfolge sein können.


Ich suche nach einer Antwort, die nicht von den Details abhängt, was sich ändert my_complex_api_function. Das Verfahren sollte für jede winzige Anpassung an das Original funktionieren complex_api_function.

actual_panda
quelle

Antworten:

3

Ich würde Folgendes empfehlen:

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

Dies ist klar und prägnant. Denken Sie aber auch daran, dass Sie bei Verwendung eines Dokumentationssystems wie Sphinx die Funktionen mit :ref:`bob`oder ähnlichem verknüpfen können .

Legorooj
quelle
1
Ich würde nicht einmal erwähnen, welcher Typ complex_api_functionfür seinen Parameter erwartet, da dies nur Informationen dupliziert (möglicherweise haben sie auch mehrere Optionen). Vermutlich ist der Benutzer des Wrappers bereits mit der ursprünglichen Funktion vertraut, und wenn nicht, können Sie sie immer auf die Originaldokumente verweisen. Wie auch immer, ich denke, dies ist der richtige Weg. Dokumentieren Sie nur, was der ursprünglichen Funktion hinzugefügt wurde , und geben Sie Details darüber an, wie dieser neue Typ in den ursprünglichen Typ konvertiert wird (diese Details könnten wichtig sein). Dh wie dieses Argument behandelt wird, um mit der ursprünglichen Funktion kompatibel zu sein.
a_guest
1
Das ist ein guter Punkt beim Verknüpfen - ich habe eine Bearbeitung für a :ref:in der Dokumentzeichenfolge hinzugefügt. Bei kleinen API-Änderungen, nach denen das OP fragt, können Benutzer die Funktionen jedoch einfacher vergleichen. In diesem Fall kann der minimale Aufwand den Endbenutzern ein wenig mehr Gewinn bringen - und wenn ich Dokumente lese, würde ich in den meisten Fällen ein 12-seitiges Dokument über ein 6-seitiges Dokument ziehen, da es so wenig einfacher zu verstehen ist.
Legorooj
5

Sie können die "Spezialisierung" der Originaldokumentation mit einem Nachtrag automatisieren . Zum Beispiel pydoc wird mit der besonderen Eigenschaft__doc__ . Sie könnten einen Dekorateur schreiben, der die ursprüngliche Funktion __doc__mit Ihrem Nachtrag automatisch überschreibt .

Zum Beispiel:

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)

oder...

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)

Wenn Sie pydoc ( pydoc3 -w my_module.py) ausführen, wird Folgendes angezeigt : Vorschau des von pydoc generierten HTML-Codes

Zusätzlicher Hinweis: Wenn Sie Python 3 verwenden, können Sie Anmerkungen verwenden , um die Art (en) Ihrer Funktionsparameter zu dokumentieren. Es bietet viele Vorteile, nicht nur die Dokumentation. Zum Beispiel:

from typing import Union

def my_complex_api_function(number_or_str: Union[int, float, str], *args, **kwargs):
Raphael Medaer
quelle
1
Dies hat den Nachteil, dass die wichtigen (neuen) Informationen am Ende der (vermutlich sehr langen) Dokumentzeichenfolge "versteckt" sind. Daher ist die Erkennbarkeit der neuen Funktion sehr gering, während sie die einzigen wertvollen Informationen sind, die zur vorhandenen Dokumentzeichenfolge hinzugefügt werden. Außerdem werden Typdeklarationen in der ursprünglichen Dokumentzeichenfolge in Konflikt gebracht. Das heißt, wenn der Benutzer die erweiterte Dokumentzeichenfolge betrachtet, wird er a : floatganz oben angezeigt und kommt nie zu dem Schluss, dass er möglicherweise auch eine strhier verwendet. Nur wenn sie zufällig bis zum Ende der Dokumente scrollen, werden sie es entdecken.
a_guest
1
Sie können den Nachtrag am Anfang statt am Ende hinzufügen ... Wie eine "Überarbeitungsnotiz" am Anfang eines Dokuments.
Raphael Medaer
1
Ein weiteres Problem ist das Duplizieren (+ Einfrieren) von Informationen. Angenommen, Sie erstellen ein Paket, das diesen Wrapper enthält, und geben Ihre Abhängigkeiten als an complex_package >= 1.1.0. Wenn Sie jetzt Ihr Paket erstellen, müssen Sie eine bestimmte Version für verwenden complex_package. Nehmen wir an, es gibt bereits complex_package==1.5.0Pypi und sie haben complex_api_functionin der Version ein neues Schlüsselwortargument hinzugefügt 1.3.0. In beiden Fällen (mit 1.1.0oder 1.5.0) haben Sie veraltete / falsche Informationen für eine Untergruppe Ihrer Benutzer im Dokument. Gleiches gilt für zukünftige Änderungen, die noch nicht öffentlich sind.
a_guest
-1

Ich bin mir nicht sicher, ob dies das ist, wonach Sie suchen, aber es hilft, die Frage vollständig zu vermeiden.

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
quelle
Vielen Dank. wrapped_api_funchat keine Dokumentzeichenfolge, daher ist das Dokumentationsproblem nicht gelöst.
actual_panda