3

(Hinweis: Dies wird gebeten, Wissen zu teilen, um Hilfe zu suchen nicht)Wie kann ich Klickbefehle mit Sphinx dokumentieren?

click ist eine beliebte Python-Bibliothek für mit CLI-Anwendungen zu entwickeln. sphinx ist eine beliebte Bibliothek zum Dokumentieren von Python-Paketen mit. One problem that some have faced integriert diese beiden Tools, sodass sie Sphinx-Dokumentation für ihre click-basierten Befehle generieren können.

Ich stieß in letzter Zeit auf dieses Problem. Ich dekorierte einige meiner Funktionen mit click.command und click.group, fügte DocStrings hinzu und generierte dann HTML-Dokumentation für sie mit der Erweiterung autodoc von Sphinx. Was ich gefunden habe, ist, dass es alle Dokumentations- und Argumentbeschreibungen für diese Befehle ausgelassen hat, weil sie zu dem Zeitpunkt, zu dem autodoc an sie gekommen ist, in Command Objekte umgewandelt wurden.

Wie kann ich meinen Code so ändern, dass die Dokumentation für meine Befehle sowohl für Endbenutzer, die auf der CLI --help ausführen, als auch für Benutzer der von Sphinx generierten Dokumentation verfügbar ist?

Antwort

2

Dekorieren Befehl Container

Eine mögliche Lösung für dieses Problem, das ich vor kurzem entdeckt haben und scheint wäre zu arbeiten, um einen Dekorateur zu beginnen definieren, die auf Klassen angewendet werden kann. Die Idee ist, dass der Programmierer Befehle als private Mitglieder einer Klasse definiert und der Dekorator ein öffentliches Funktionsmember der Klasse erstellt, die auf dem Callback des Befehls basiert. Zum Beispiel würde eine Klasse Foo, die einen Befehl _bar enthält, eine neue Funktion erhalten bar (unter der Annahme, dass Foo.bar nicht bereits existiert).

Diese Operation belässt die ursprünglichen Befehle so, wie sie sind. Daher sollte der vorhandene Code nicht unterbrochen werden. Da diese Befehle privat sind, sollten sie in der generierten Dokumentation weggelassen werden. Die darauf basierenden Funktionen sollten jedoch in der Dokumentation aufgrund der Öffentlichkeit erscheinen.

def ensure_cli_documentation(cls): 
    """ 
    Modify a class that may contain instances of :py:class:`click.BaseCommand` 
    to ensure that it can be properly documented (e.g. using tools such as Sphinx). 

    This function will only process commands that have private callbacks i.e. are 
    prefixed with underscores. It will associate a new function with the class based on 
    this callback but without the leading underscores. This should mean that generated 
    documentation ignores the command instances but includes documentation for the functions 
    based on them. 

    This function should be invoked on a class when it is imported in order to do its job. This 
    can be done by applying it as a decorator on the class. 

    :param cls: the class to operate on 
    :return: `cls`, after performing relevant modifications 
    """ 
    for attr_name, attr_value in dict(cls.__dict__).items(): 
     if isinstance(attr_value, click.BaseCommand) and attr_name.startswith('_'): 
      cmd = attr_value 
      try: 
       # noinspection PyUnresolvedReferences 
       new_function = copy.deepcopy(cmd.callback) 
      except AttributeError: 
       continue 
      else: 
       new_function_name = attr_name.lstrip('_') 
       assert not hasattr(cls, new_function_name) 
       setattr(cls, new_function_name, new_function) 

    return cls 

Probleme mit Befehlen in Klassen Vermeidung

Der Grund, dass diese Lösung innerhalb von Klassen Befehle sind davon ausgegangen, weil das ist, wie die meisten meiner Befehle im Projekt I definiert sind aktuell gerade arbeite - ich lade die meisten meiner Befehle als Plugins in Unterklassen von yapsy.IPlugin.IPlugin. Wenn Sie die Callbacks für Befehle als Klasseninstanzmethoden definieren möchten, können Sie auf ein Problem stoßen, bei dem click den self-Parameter nicht zu Ihren Befehlsrückrufen liefert, wenn Sie versuchen, Ihre CLI auszuführen.Dies kann durch currying Ihre Rückrufe gelöst werden, wie unten:

class Foo: 
    def _curry_instance_command_callbacks(self, cmd: click.BaseCommand): 
     if isinstance(cmd, click.Group): 
      commands = [self._curry_instance_command_callbacks(c) for c in cmd.commands.values()] 
      cmd.commands = {} 
      for subcommand in commands: 
       cmd.add_command(subcommand) 

     try: 
      if cmd.callback: 
       cmd.callback = partial(cmd.callback, self) 

      if cmd.result_callback: 
       cmd.result_callback = partial(cmd.result_callback, self) 
     except AttributeError: 
      pass 

     return cmd 

Beispiel

das Putting alles zusammen:

from functools import partial 

import click 
from click.testing import CliRunner 
from doc_inherit import class_doc_inherit 


def ensure_cli_documentation(cls): 
    """ 
    Modify a class that may contain instances of :py:class:`click.BaseCommand` 
    to ensure that it can be properly documented (e.g. using tools such as Sphinx). 

    This function will only process commands that have private callbacks i.e. are 
    prefixed with underscores. It will associate a new function with the class based on 
    this callback but without the leading underscores. This should mean that generated 
    documentation ignores the command instances but includes documentation for the functions 
    based on them. 

    This function should be invoked on a class when it is imported in order to do its job. This 
    can be done by applying it as a decorator on the class. 

    :param cls: the class to operate on 
    :return: `cls`, after performing relevant modifications 
    """ 
    for attr_name, attr_value in dict(cls.__dict__).items(): 
     if isinstance(attr_value, click.BaseCommand) and attr_name.startswith('_'): 
      cmd = attr_value 
      try: 
       # noinspection PyUnresolvedReferences 
       new_function = cmd.callback 
      except AttributeError: 
       continue 
      else: 
       new_function_name = attr_name.lstrip('_') 
       assert not hasattr(cls, new_function_name) 
       setattr(cls, new_function_name, new_function) 

    return cls 


@ensure_cli_documentation 
@class_doc_inherit 
class FooCommands(click.MultiCommand): 
    """ 
    Provides Foo commands. 
    """ 

    def __init__(self, *args, **kwargs): 
     super().__init__(*args, **kwargs) 
     self._commands = [self._curry_instance_command_callbacks(self._calc)] 

    def list_commands(self, ctx): 
     return [c.name for c in self._commands] 

    def get_command(self, ctx, cmd_name): 
     try: 
      return next(c for c in self._commands if c.name == cmd_name) 
     except StopIteration: 
      raise click.UsageError('Undefined command: {}'.format(cmd_name)) 

    @click.group('calc', help='mathematical calculation commands') 
    def _calc(self): 
     """ 
     Perform mathematical calculations. 
     """ 
     pass 

    @_calc.command('add', help='adds two numbers') 
    @click.argument('x', type=click.INT) 
    @click.argument('y', type=click.INT) 
    def _add(self, x, y): 
     """ 
     Print the sum of x and y. 

     :param x: the first operand 
     :param y: the second operand 
     """ 
     print('{} + {} = {}'.format(x, y, x + y)) 

    @_calc.command('subtract', help='subtracts two numbers') 
    @click.argument('x', type=click.INT) 
    @click.argument('y', type=click.INT) 
    def _subtract(self, x, y): 
     """ 
     Print the difference of x and y. 

     :param x: the first operand 
     :param y: the second operand 
     """ 
     print('{} - {} = {}'.format(x, y, x - y)) 

    def _curry_instance_command_callbacks(self, cmd: click.BaseCommand): 
     if isinstance(cmd, click.Group): 
      commands = [self._curry_instance_command_callbacks(c) for c in cmd.commands.values()] 
      cmd.commands = {} 
      for subcommand in commands: 
       cmd.add_command(subcommand) 

     if cmd.callback: 
      cmd.callback = partial(cmd.callback, self) 

     return cmd 


@click.command(cls=FooCommands) 
def cli(): 
    pass 


def main(): 
    print('Example: Adding two numbers') 
    runner = CliRunner() 
    result = runner.invoke(cli, 'calc add 1 2'.split()) 
    print(result.output) 

    print('Example: Printing usage') 
    result = runner.invoke(cli, 'calc add --help'.split()) 
    print(result.output) 


if __name__ == '__main__': 
    main() 

main() Rennen, erhalte ich diese Ausgabe:

Example: Adding two numbers 
1 + 2 = 3 

Example: Printing usage 
Usage: cli calc add [OPTIONS] X Y 

    adds two numbers 

Options: 
    --help Show this message and exit. 


Process finished with exit code 0 

Diesen Throug ausführen h Sphinx, ich kann die Dokumentation hierzu in meinem Browser ansehen:

Sphinx documentation

Verwandte Themen