3

(참고 : 도움을 요청하지 않고 지식을 공유하라는 메시지입니다.)스핑크스를 사용하여 클릭 명령을 문서화하려면 어떻게해야합니까?

click은 CLI 응용 프로그램을 개발할 때 널리 사용되는 Python 라이브러리입니다. sphinx은 Python 패키지를 문서화하기위한 인기있는 라이브러리입니다. One problem that some have faced은 두 도구를 통합하여 클릭 기반 명령에 대한 Sphinx 문서를 생성 할 수 있습니다.

최근에이 문제가 발생했습니다. 일부 기능을 click.commandclick.group으로 꾸며서 docstrings을 추가 한 다음 Sphinx의 autodoc 확장명을 사용하여 HTML 문서를 생성했습니다. 내가 발견 한 것은 autodoc이 그들에게 주어진 시간에 Command 오브젝트로 변환 되었기 때문에이 명령에 대한 모든 문서와 인수 설명이 생략되었다는 것입니다.

최종 사용자가 CLI에서 --help을 실행할 때 내 명령에 대한 설명서를 사용할 수있게 만들고 내 명령을 문서화 할 수 있도록하려면 어떻게해야합니까? 또한 스핑크스 생성 문서를 탐색하는 사람들에게도 내 코드를 수정할 수 있습니까?

답변

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를 실행하려고 할 때 click이 명령 콜백에 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 

이 모두 함께이 퍼팅 :

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 
:

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() 실행,이 출력을 얻을

이 실행 h 스핑크스, 내 브라우저에서이 문서를 볼 수 있습니다 :

Sphinx documentation