3

(注:這是被要求分享知識,而不是尋求幫助)如何使用Sphinx記錄點擊命令?

click是與發展中國家CLI應用的流行Python庫。 sphinx是用於記錄Python包的流行庫。 One problem that some have faced正在整合這兩個工具,以便他們可以生成基於點擊命令的Sphinx文檔。

我最近遇到這個問題。我用click.commandclick.group修飾了我的一些函數,爲它們添加了文檔,然後使用Sphinx的autodoc擴展名爲它們生成了HTML文檔。我發現它省略了這些命令的所有文檔和參數描述,因爲它們在autodoc到達它們時已被轉換爲Command對象。

如何修改我的代碼,以使我的命令的文檔可供最終用戶在CLI上運行--help時使用,同時也適用於瀏覽Sphinx生成的文檔的用戶?

回答

2

裝飾命令容器

一個可能的解決這個問題,我已經最近發現,似乎工作將開始定義可應用於類裝飾器。這個想法是,程序員將命令定義爲類的私有成員,並且裝飾器創建基於命令回調的類的公共函數成員。例如,包含命令_bar的類Foo將獲得新功能bar(假設Foo.bar尚不存在)。

該操作保留原來的命令,因此它不應該破壞現有的代碼。由於這些命令是私有的,因此應在生成的文檔中省略它們。但是,基於這些功能的功能應該在公開的文檔中出現。

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 

避免問題與命令在班

,這種解決方案假定命令是內部類的原因是因爲這是怎麼了我的大部分命令在我目前從事的項目的定義 - 我將大部分命令加載爲yapsy.IPlugin.IPlugin的子類中包含的插件。如果要將命令的回調函數定義爲類實例方法,則當您嘗試運行CLI時,可能會遇到點擊時不會爲命令回調提供self參數的問題。這可以通過討好你的回調來解決,象下面這樣:

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 

把所有這些組合起來:

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(),我得到這樣的輸出:

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 

運行此通道^ h獅身人面像,我可以在瀏覽器中查看該文檔:

Sphinx documentation