自定义argparse帮助信息

60

我已经编写了下面的示例代码以展示我的问题。

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('-v', '--version', action='version',
                    version='%(prog)s 1.0')
parser.parse_args()

这将生成以下帮助信息。

$ python foo.py --help
usage: foo.py [-h] [-v]

optional arguments:
  -h, --help     show this help message and exit
  -v, --version  show program's version number and exit

我希望自定义这个帮助输出,使其将所有短语和句子都大写,并在句子后面加上句号。换句话说,我希望生成的帮助信息如下所示。

$ python foo.py --help
Usage: foo.py [-h] [-v]

Optional arguments:
  -h, --help     Show this help message and exit.
  -v, --version  Show program's version number and exit.

我能否使用argparse API来控制这个东西?如果可以,怎么做呢?请给出一个小例子以示说明。


1
你尝试过设置help吗? - jonrsharpe
2
哦,我明白了 - 那么你可以将 add_help 设置为 False 并手动执行。但是小写字母是这些事情的惯例。 - jonrsharpe
1
@jonrsharpe,你怎么知道的?git 大写。curl 大写。apt-get 大写。gcc 大写。如果有一个约定(我怀疑是否有),我不确定 argparse 是否在正确的一边。 - Paul Draper
@PaulDraper 我应该更加具体地说明,这是在 argparse 中的惯例(以及 Python 中的通用惯例,在 python3 --help 中会显示小写的标记帮助)。 - jonrsharpe
你能否直接编辑 parser.format_help 生成的文本呢?比如说 parser.usage = edit(parser.format_help()),然后编写一个函数 edit 来解析每一行并将帮助字符串转换为标题格式。 - SurpriseDog
4个回答

61
首先:将这些短语大写是违背惯例的,而argparse并没有提供方便更改这些字符串的工具。你有三种不同类别的字符串:帮助格式化器中的样板文本、部分标题以及每个特定选项的帮助文本。所有这些字符串都可以本地化;你可以通过gettext()模块支持为所有这些字符串提供“大写”翻译。话虽如此,如果你足够决心并稍微阅读一下源代码,你可以直接替换所有这些字符串。 version操作包括默认的help文本,但你可以通过设置help参数来提供自己的文本。同样适用于help操作;如果你将add_help参数设置为False,则可以手动添加该操作。
parser = argparse.ArgumentParser(add_help=False)

parser.add_argument('-v', '--version', action='version',
                    version='%(prog)s 1.0', help="Show program's version number and exit.")
parser.add_argument('-h', '--help', action='help', default=argparse.SUPPRESS,
                    help='Show this help message and exit.')

接下来,optional arguments消息是一个组标题;每个解析器都有两个默认组,一个用于位置参数,另一个用于可选参数。您可以通过属性_positionals_optionals访问这些组,两者都具有title属性:

parser._positionals.title = 'Positional arguments'
parser._optionals.title = 'Optional arguments'

请注意,访问以下划线开头的名称,您将进入模块未记录的私有API,您的代码可能会在未来的更新中出现问题。

最后,要更改usage字符串,您必须子类化帮助格式化程序;将子类作为formatter_class参数传递:

class CapitalisedHelpFormatter(argparse.HelpFormatter):
    def add_usage(self, usage, actions, groups, prefix=None):
        if prefix is None:
            prefix = 'Usage: '
        return super(CapitalisedHelpFormatter, self).add_usage(
            usage, actions, groups, prefix)

parser = argparse.ArgumentParser(formatter_class=CapitalisedHelpFormatter)

演示,将它们组合在一起:

>>> import argparse
>>> class CapitalisedHelpFormatter(argparse.HelpFormatter):
...     def add_usage(self, usage, actions, groups, prefix=None):
...         if prefix is None:
...             prefix = 'Usage: '
...         return super(CapitalisedHelpFormatter, self).add_usage(
...             usage, actions, groups, prefix)
...
>>> parser = argparse.ArgumentParser(add_help=False, formatter_class=CapitalisedHelpFormatter)
>>> parser._positionals.title = 'Positional arguments'
>>> parser._optionals.title = 'Optional arguments'
>>> parser.add_argument('-v', '--version', action='version',
...                     version='%(prog)s 1.0', help="Show program's version number and exit.")
_VersionAction(option_strings=['-v', '--version'], dest='version', nargs=0, const=None, default='==SUPPRESS==', type=None, choices=None, help="Show program's version number and exit.", metavar=None)
>>> parser.add_argument('-h', '--help', action='help', default=argparse.SUPPRESS,
...                     help='Show this help message and exit.')
_HelpAction(option_strings=['-h', '--help'], dest='help', nargs=0, const=None, default='==SUPPRESS==', type=None, choices=None, help='Show this help message and exit.', metavar=None)
>>> print(parser.format_help())
Usage: [-v] [-h]

Optional arguments:
  -v, --version  Show program's version number and exit.
  -h, --help     Show this help message and exit.

1
我认为即使是 argparse.HelpFormatter 的非下划线方法也是私有 API:https://github.com/python/cpython/blob/v3.6.5/Lib/argparse.py#L58 “还要注意的是,HelpFormatter 和 RawDescriptionHelpFormatter 只被视为对象名称公共 -- 格式化程序对象的 API 仍被视为实现细节。” - Ciro Santilli OurBigBook.com
5
正如保罗·德拉珀在另一条评论中指出的那样,关于命令行程序的大写惯例实际上并没有一个统一的规定,因此你的开头语有些过于绝对。运行 --help 命令可以查看 git、curl、vim、gcc、perl、ruby、diff 和 rsync 等程序采用的不同方法。甚至 Python 本身也与 argparse 有略微不同的路径:大多数都是小写字母,但有少数例外("选项和参数"标题以及可能是 "-X dev" 的项目)。 - FMc
@FMc:我说的不是流行命令行工具之间的共同点,而是英语语法的惯例。流行开源软件工具背后的程序员来自各种文化背景,对英语及其(有时复杂的)大写规则的掌握程度也不同,因此在这方面存在很多变异性是不足为奇的。 - Martijn Pieters

8

不要依赖于内部API(这些API可能会在没有通知的情况下更改),而是使用仅公共API的替代方法。虽然更复杂,但反过来却可以使您对打印内容具有最大控制权:

class ArgumentParser(argparse.ArgumentParser):

    def __init__(self, *args, **kwargs):
        super(ArgumentParser, self).__init__(*args, **kwargs)
        self.program = { key: kwargs[key] for key in kwargs }
        self.options = []

    def add_argument(self, *args, **kwargs):
        super(ArgumentParser, self).add_argument(*args, **kwargs)
        option = {}
        option["flags"] = [ item for item in args ]
        for key in kwargs:
            option[key] = kwargs[key]
        self.options.append(option)

    def print_help(self):
        # Use data stored in self.program/self.options to produce
        # custom help text

工作原理:

  • 利用argparse.ArgumentParser构造函数来捕获并存储程序信息(例如描述,使用情况)到self.program
  • 利用argparse.ArgumentParser.add_argument()来捕获并存储添加的参数(例如标志,帮助信息,默认值)到self.options
  • 重新定义argparse.ArgumentParser.print_help()并使用之前存储的程序信息/参数来生成帮助文本

以下是一个包含一些常见用例的完整示例。注意,这并不是完整的(例如没有支持位置参数或具有多个参数选项的支持),但应该能够提供对可能性的良好印象:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import os
import sys
import argparse
import textwrap

class ArgumentParser(argparse.ArgumentParser):

    def __init__(self, *args, **kwargs):
        super(ArgumentParser, self).__init__(*args, **kwargs)
        self.program = { key: kwargs[key] for key in kwargs }
        self.options = []

    def add_argument(self, *args, **kwargs):
        super(ArgumentParser, self).add_argument(*args, **kwargs)
        option = {}
        option["flags"] = [ item for item in args ]
        for key in kwargs:
            option[key] = kwargs[key]
        self.options.append(option)

    def print_help(self):
        wrapper = textwrap.TextWrapper(width=80)

        # Print usage
        if "usage" in self.program:
            print("Usage: %s" % self.program["usage"])
        else:
            usage = []
            for option in self.options:
                usage += [ "[%s %s]" % (item, option["metavar"]) if "metavar" in option else "[%s %s]" % (item, option["dest"].upper()) if "dest" in option else "[%s]" % item for item in option["flags"] ]
            wrapper.initial_indent = "Usage: %s " % os.path.basename(sys.argv[0])
            wrapper.subsequent_indent = len(wrapper.initial_indent) * " "
            output = str.join(" ", usage)
            output = wrapper.fill(output)
            print(output)
        print()

        # Print description
        if "description" in self.program:
            print(self.program["description"])
            print()

        # Print options
        print("Options:")
        maxlen = 0
        for option in self.options:
            option["flags2"] = str.join(", ", [ "%s %s" % (item, option["metavar"]) if "metavar" in option else "%s %s" % (item, option["dest"].upper()) if "dest" in option else item for item in option["flags"] ])
            if len(option["flags2"]) > maxlen:
                maxlen = len(option["flags2"])
        for option in self.options:
            template = "  %-" + str(maxlen) + "s  "
            wrapper.initial_indent = template % option["flags2"]
            wrapper.subsequent_indent = len(wrapper.initial_indent) * " "
            if "help" in option and "default" in option:
                output = option["help"]
                output += " (default: '%s')" % option["default"] if isinstance(option["default"], str) else " (default: %s)" % str(option["default"])
                output = wrapper.fill(output)
            elif "help" in option:
                output = option["help"]
                output = wrapper.fill(output)
            elif "default" in option:
                output = "Default: '%s'" % option["default"] if isinstance(option["default"], str) else "Default: %s" % str(option["default"])
                output = wrapper.fill(output)
            else:
                output = wrapper.initial_indent
            print(output)

# Main
if (__name__ == "__main__"):
    #parser = argparse.ArgumentParser(description="Download program based on some library.", argument_default=argparse.SUPPRESS, allow_abbrev=False, add_help=False)
    #parser = argparse.ArgumentParser(usage="%s [OPTION]..." % os.path.basename(sys.argv[0]), description="Download program based on some library.", argument_default=argparse.SUPPRESS, allow_abbrev=False, add_help=False)
    #parser = ArgumentParser(usage="%s [OPTION]..." % os.path.basename(sys.argv[0]), description="Download program based on some library.", argument_default=argparse.SUPPRESS, allow_abbrev=False, add_help=False)
    parser = ArgumentParser(description="Download program based on some library.", argument_default=argparse.SUPPRESS, allow_abbrev=False, add_help=False)

    parser.add_argument("-c", "--config-file", action="store", dest="config_file", metavar="file", type=str, default="config.ini")
    parser.add_argument("-d", "--database-file", action="store", dest="database_file", metavar="file", type=str, help="SQLite3 database file to read/write", default="database.db")
    parser.add_argument("-l", "--log-file", action="store", dest="log_file", metavar="file", type=str, help="File to write log to", default="debug.log")
    parser.add_argument("-f", "--data-file", action="store", dest="data_file", metavar="file", type=str, help="Data file to read", default="data.bin")
    parser.add_argument("-t", "--threads", action="store", dest="threads", type=int, help="Number of threads to spawn", default=3)
    parser.add_argument("-p", "--port", action="store", dest="port", type=int, help="TCP port to listen on for access to the web interface", default="12345")
    parser.add_argument("--max-downloads", action="store", dest="max_downloads", metavar="value", type=int, help="Maximum number of concurrent downloads", default=5)
    parser.add_argument("--download-timeout", action="store", dest="download_timeout", metavar="value", type=int, help="Download timeout in seconds", default=120)
    parser.add_argument("--max-requests", action="store", dest="max_requests", metavar="value", type=int, help="Maximum number of concurrent requests", default=10)
    parser.add_argument("--request-timeout", action="store", dest="request_timeout", metavar="value", type=int, help="Request timeout in seconds", default=60)
    parser.add_argument("--main-interval", action="store", dest="main_interval", metavar="value", type=int, help="Main loop interval in seconds", default=60)
    parser.add_argument("--thread-interval", action="store", dest="thread_interval", metavar="value", type=int, help="Thread loop interval in milliseconds", default=500)
    parser.add_argument("--console-output", action="store", dest="console_output", metavar="value", type=str.lower, choices=["stdout", "stderr"], help="Output to use for console", default="stdout")
    parser.add_argument("--console-level", action="store", dest="console_level", metavar="value", type=str.lower, choices=["debug", "info", "warning", "error", "critical"], help="Log level to use for console", default="info")
    parser.add_argument("--logfile-level", action="store", dest="logfile_level", metavar="value", type=str.lower, choices=["debug", "info", "warning", "error", "critical"], help="Log level to use for log file", default="info")
    parser.add_argument("--console-color", action="store", dest="console_color", metavar="value", type=bool, help="Colorized console output", default=True)
    parser.add_argument("--logfile-color", action="store", dest="logfile_color", metavar="value", type=bool, help="Colorized log file output", default=False)
    parser.add_argument("--log-template", action="store", dest="log_template", metavar="value", type=str, help="Template to use for log lines", default="[%(created)d] [%(threadName)s] [%(levelname)s] %(message)s")
    parser.add_argument("-h", "--help", action="help", help="Display this message")

    args = parser.parse_args(["-h"])

生成的输出:

Usage: argparse_custom_usage.py [-c file] [--config-file file] [-d file]
                                [--database-file file] [-l file] [--log-file
                                file] [-f file] [--data-file file] [-t THREADS]
                                [--threads THREADS] [-p PORT] [--port PORT]
                                [--max-downloads value] [--download-timeout
                                value] [--max-requests value] [--request-timeout
                                value] [--main-interval value] [--thread-
                                interval value] [--console-output value]
                                [--console-level value] [--logfile-level value]
                                [--console-color value] [--logfile-color value]
                                [--log-template value] [-h] [--help]

Download program based on some library.

Options:
  -c file, --config-file file    Default: 'config.ini'
  -d file, --database-file file  SQLite3 database file to read/write (default:
                                 'database.db')
  -l file, --log-file file       File to write log to (default: 'debug.log')
  -f file, --data-file file      Data file to read (default: 'data.bin')
  -t THREADS, --threads THREADS  Number of threads to spawn (default: 3)
  -p PORT, --port PORT           TCP port to listen on for access to the web
                                 interface (default: '12345')
  --max-downloads value          Maximum number of concurrent downloads
                                 (default: 5)
  --download-timeout value       Download timeout in seconds (default: 120)
  --max-requests value           Maximum number of concurrent requests (default:
                                 10)
  --request-timeout value        Request timeout in seconds (default: 60)
  --main-interval value          Main loop interval in seconds (default: 60)
  --thread-interval value        Thread loop interval in milliseconds (default:
                                 500)
  --console-output value         Output to use for console (default: 'stdout')
  --console-level value          Log level to use for console (default: 'info')
  --logfile-level value          Log level to use for log file (default: 'info')
  --console-color value          Colorized console output (default: True)
  --logfile-color value          Colorized log file output (default: False)
  --log-template value           Template to use for log lines (default:
                                 '[%(created)d] [%(threadName)s] [%(levelname)s]
                                 %(message)s')
  -h, --help                     Display this message

编辑:

如果已经显著扩展了示例,并将继续在GitHub上进行。


这个答案有点复杂,但更加安全。谢谢。 - Hayk Avetisyan

6

Martijn提供了一些解决方案,包括提供help参数和自定义Formatter类。

另一个部分的解决方案是在创建参数后修改帮助字符串。 add_argument创建并返回一个包含参数和默认值的Action对象。您可以保存此链接并修改Action。您还可以获取这些操作的列表,并对其进行操作。

让我举个例子,对于一个具有默认帮助和另一个参数的简单解析器,操作列表如下:

In [1064]: parser._actions
Out[1064]: 
[_HelpAction(option_strings=['-h', '--help'], dest='help', nargs=0, const=None, default='==SUPPRESS==', type=None, choices=None, help='show this help message and exit', metavar=None),
 _StoreAction(option_strings=['-f', '--foo'], dest='foo', nargs=None, const=None, default=None, type=None, choices=None, help=None, metavar=None)]

我可以查看和修改任何一个元素的help属性:
In [1065]: parser._actions[0].help
Out[1065]: 'show this help message and exit'
In [1066]: parser._actions[0].help='Show this help message and exit.'

生成此帮助文档:
In [1067]: parser.parse_args(['-h'])
usage: ipython3 [-h] [-f FOO]    
optional arguments:
  -h, --help         Show this help message and exit.
  -f FOO, --foo FOO

使用parser._actions列表使用了一个“私有”属性,一些人认为这是不明智的。但在Python中,公共/私有区分并不严格,可以小心地打破它。马特因通过访问parser._positionals.title来实现这一点。
改变该组标题的另一种方法是使用自定义参数组。
ogroup=parser.add_argument_group('Correct Optionals Title')
ogroup.add_argument('-v',...)
ogroup.add_argument('-h',...)

0
这个自定义的 `argparse.ArgumentParser` 可以用来将输出消息大写,它还可以与使用 `add_subparsers()` 创建的子解析器一起使用。
适用于 Python 3.11,但不知道这种情况能持续多久。
import argparse


class ArgumentParser(argparse.ArgumentParser):
    """Custom `argparse.ArgumentParser` which capitalizes the output message."""

    class _ArgumentGroup(argparse._ArgumentGroup):
        def __init__(self, *args, **kwargs) -> None:
            super().__init__(*args, **kwargs)
            self.title = self.title and self.title.title()

    class _HelpFormatter(argparse.RawDescriptionHelpFormatter):
        def _format_usage(self, *args, **kwargs) -> str:
            return super()._format_usage(*args, **kwargs).replace("usage:", "Usage:", 1)

        def _format_action_invocation(self, action: argparse.Action) -> str:
            action.help = action.help and (action.help[0].upper() + action.help[1:])
            return super()._format_action_invocation(action)

    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs, formatter_class=self._HelpFormatter)

    def add_argument_group(self, *args, **kwargs) -> _ArgumentGroup:
        group = self._ArgumentGroup(self, *args, **kwargs)
        self._action_groups.append(group)
        return group

示例输出:

app@01d3adfb794b:/usr/local/src/app$ app --help
Usage: app [-h] [-v] [--debug] [command] ...

Options:
  -h, --help          Show this help message and exit
  -v, --version       Show program's version number and exit
  --debug, --verbose  Show more log

Commands:
    database          Manage the database
    user              Manage the users
    plan              Create a plan for the user
    info              Get users' info

Run 'app COMMAND --help' for more information on a command

app@01d3adfb794b:/usr/local/src/app$ app user --help
Usage: app user [-h] [-a] [-d] username [username ...]

Positional Arguments:
  username              The user's username. Multiple usernames could be specified

Options:
  -h, --help            Show this help message and exit
  -a, --add             Add a user
  -d, --delete          Delete a user

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接