click
是一个流行的Python库,用于开发CLI应用程序。 sphinx
是一个用于记录Python包的流行库。 One problem that some have faced正在整合这两个工具,以便他们可以为基于点击的命令生成Sphinx文档。
我最近遇到了这个问题。我使用click.command
和click.group
修饰了我的一些函数,向其添加了文档字符串,然后使用Sphinx的autodoc
扩展为它们生成了HTML文档。我发现它省略了这些命令的所有文档和参数描述,因为它们在autodoc到达它们时已被转换为Command
个对象。
如何修改我的代码,使最终用户在CLI上运行--help
时,以及浏览Sphinx生成的文档的人员可以使用我的命令文档?
答案 0 :(得分:2)
装饰命令容器
我最近发现并且似乎工作的这个问题的一个可能的解决方案是开始定义可以应用于类的装饰器。这个想法是程序员将命令定义为类的私有成员,并且装饰器创建基于命令的回调的类的公共函数成员。例如,包含命令Foo
的类_bar
将获得新函数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
示例强>
把这一切放在一起:
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
通过Sphinx运行,我可以在浏览器中查看相关文档:
答案 1 :(得分:2)
您现在可以为此使用狮身人面像扩展名sphinx-click。它可以为带有选项和参数说明的嵌套命令生成文档。输出将类似于您运行--help
时的情况。
pip install sphinx-click
conf.py
文件中启用插件:extensions = ['sphinx_click.ext']
.. click:: module:parser
:prog: hello-world
:show-nested:
有一个简单的click
应用程序,该应用程序在hello_world
模块中定义:
import click
@click.group()
def greet():
"""A sample command group."""
pass
@greet.command()
@click.argument('user', envvar='USER')
def hello(user):
"""Greet a user."""
click.echo('Hello %s' % user)
@greet.command()
def world():
"""Greet the world."""
click.echo('Hello world!')
要记录所有子命令,我们将在下面使用带有:show-nested:
选项的代码
.. click:: hello_world:greet
:prog: hello-world
:show-nested:
在构建文档之前,请确保通过sys.path
安装软件包或手动添加软件包来确保setuptools
中模块和任何其他依赖项都可用。
构建后,我们将获得以下信息: generated docs
扩展名documentation中提供了有关可用选项的更多详细信息