在Setuptools入口点中使用argparse

5 投票
2 回答
2606 浏览
提问于 2025-04-17 20:55

我正在写一个脚本,想用Setuptools来分发它。我已经把这个脚本添加到了我的setup.py文件中的entry_points部分。

根据Setuptools的文档:

你指定的函数会在没有参数的情况下被调用,它的返回值会传递给sys.exit(),所以你可以返回一个错误级别或要打印到标准错误的信息

因为这个方法会返回而不是直接退出,所以它更容易进行测试。为了方便测试,我在方法中接受参数,默认使用sys.argv。到目前为止,一切都很好。

问题出现在添加了argparse之后。当argparse无法解析参数时,它会调用sys.exit。我其实更希望argparse不要这样做,因为这个退出是由Setuptools的包装器来处理的。我想到的第一个解决办法是重写argparse.ArgumentParser,但后来我看到这个:

# ===============
# Exiting methods
# ===============
def exit(self, status=0, message=None):
    if message:
        self._print_message(message, _sys.stderr)
    _sys.exit(status)

def error(self, message):
    """error(message: string)

    Prints a usage message incorporating the message to stderr and
    exits.

    If you override this in a subclass, it should not return -- it
    should either exit or raise an exception.
    """
    self.print_usage(_sys.stderr)
    self.exit(2, _('%s: error: %s\n') % (self.prog, message))

文档中提到我不应该返回,而是应该抛出一个异常。我该如何解决这个问题呢?

如果我没有解释得够清楚,下面是主要方法的代码:

def main(args=sys.argv):
    parser = ArgumentParser(prog='spam')

    # parser is configured here

    parsed = parser.parse_args(args)

    # Parsed args are used here

2 个回答

0

如果你正在开始一个新项目,或者有时间对现有代码进行一些改进,那么你可以考虑使用Click这个库。Click库有一些很不错的功能,比如与setuptools的集成和'可测试性'等。

下面是文档中的一个示例/测试代码片段,它不仅创建了一个简单的命令行界面,还立即对其进行了测试:

import click
from click.testing import CliRunner

def test_hello_world():
    @click.command()
    @click.argument('name')
    def hello(name):
        click.echo('Hello %s!' % name)

    runner = CliRunner()
    result = runner.invoke(hello, ['Peter'])
    assert result.exit_code == 0
    assert result.output == 'Hello Peter!\n'
2

你不想在出错的时候直接用 return 的原因是,解析器会继续进行解析。有些错误可能在最后才出现(比如未解析的字符串),而有些错误可能在一开始就会出现(比如第一个参数字符串类型不对)。如果你在错误处理的方法里用 return,那么 parse_args 的行为就会变得不可预测。通常情况下,你希望解析器停止工作,然后把控制权交回给你的代码。

你应该把 parse_args() 的调用放在一个 try: except SystemExit: 的代码块里。我经常会用这样的测试脚本:

for test in ['-o FILE',
    ...
         ]:
    print(test)
    try:
        print(parser.parse_args(test.split()))
    except SystemExit:
        pass

你可以使用 error 和/或 exit 来返回其他类型的异常。它们也可能会跳过使用信息。不过,不管怎样,你需要在你的包装器里捕获这个异常。

撰写回答