跳转到主要内容

带有CLI支持的简单基于字典的脚本配置

项目描述

GitlabCIPipeline GitlabCICoverage Appveyor Pypi PypiDownloads

阅读文档

https://scriptconfig.readthedocs.io

Gitlab (主要)

https://gitlab.kitware.com/utils/scriptconfig

Github (镜像)

https://github.com/Kitware/scriptconfig

Pypi

https://pypi.ac.cn/project/scriptconfig

scriptconfig 的目标是通过简单地定义一个字典来定义默认配置,然后允许通过以下方式修改该配置:

  1. 使用另一个Python字典(例如 kwargs)更新它

  2. 读取YAML/JSON配置文件,或者

  3. 检查 sys.argv 上的值,在这种情况下,我们提供了一个强大的命令行界面(CLI)。

创建脚本配置的最简单方法是创建一个继承自 scriptconfig.DataConfig 的类。然后,使用类变量定义预期的键和默认值。

import scriptconfig as scfg

class ExampleConfig(scfg.DataConfig):
    """
    The docstring will be the description in the CLI help
    """

    # Wrap defaults with `Value` to provide metadata

    option1 = scfg.Value('default1', help='option1 help')
    option2 = scfg.Value('default2', help='option2 help')
    option3 = scfg.Value('default3', help='option3 help')

    # Wrapping a default with `Value` is optional

    option4 = 'default4'

配置对象的实例将类似于数据类,但它还实现了方法来实现鸭子类型字典。因此,可以将scriptconfig对象放入使用现有字典配置或现有argparse命名空间配置的代码中。

# Use as a dictionary with defaults
config = ExampleConfig(option1=123)
print(config)

# Can access items like a dictionary
print(config['option1'])

# OR Can access items like a namespace
print(config.option1)

使用.cli类方法创建扩展的argparse命令行界面。cli方法的选项类似于argparse.ArgumentParser.parse_args

# Use as a argparse CLI
config = ExampleConfig.cli(argv=['--option2=overruled'])
print(config)

在所有这些之后,如果你仍然不喜欢scriptconfig,或者不能将其作为生产环境中的依赖项使用,你可以要求它通过print(ExampleConfig().port_to_argparse())转换为纯argparse,它将打印出

import argparse
parser = argparse.ArgumentParser(
    prog='ExampleConfig',
    description='The docstring will be the description in the CLI help',
    formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument('--option1', help='option1 help', default='default1', dest='option1', required=False)
parser.add_argument('--option2', help='option2 help', default='default2', dest='option2', required=False)
parser.add_argument('--option3', help='option3 help', default='default3', dest='option3', required=False)
parser.add_argument('--option4', help='', default='default4', dest='option4', required=False)

当然,上述操作也移除了scriptconfig的一些额外功能——因此它并不是完全1对1,但非常接近。它也是一个很好的工具,可以将对argparse的现有直觉转移到scriptconfig

同样,还有一个方法可以接受现有的ArgumentParser作为输入,并生成scriptconfig定义。给定上述parser对象,print(scg.Config.port_from_argparse(parser, style))将打印出

import ubelt as ub
import scriptconfig as scfg

class MyConfig(scfg.DataConfig):
    """
    The docstring will be the description in the CLI help
    """
    option1 = scfg.Value('default1', help='option1 help')
    option2 = scfg.Value('default2', help='option2 help')
    option3 = scfg.Value('default3', help='option3 help')
    option4 = scfg.Value('default4', help='')

目标

我们的想法是能够用简单的配置开始编写简单的程序,并允许它以最小的重构方式演进。在早期阶段,我们将坚持要求尽量减少样板代码,但随着程序的发展,我们将添加样板代码来增强程序的功能。

当我们开始编码时,我们应该努力做到像这样

def my_function():

    config = {
        'simple_option1': 1,
        'simple_option2': 2,
    }

    # Early algorithmic and debugging logic
    ...

随着我们代码的演进,我们可以像这样插入scriptconfig

def my_function():

    default_config = {
        'simple_option1': 1,
        'simple_option2': 2,
    }

    import scriptconfig
    class MyConfig(scriptconfig.DataConfig):
        __default__ = default_config

    config = MyConfig()

    # Transition algorithmic and debugging logic
    ...

这看起来并不美观,但它让我们能够立即获得一个相当先进的CLI(即通过调用.cli类方法),而不会对代码的简洁性造成任何重大牺牲。然而,随着项目的演进,我们可能最终希望重构我们的CLI以完全控制配置和CLI中的元数据。Scriptconfig也有一个工具可以帮助我们做到这一点。给定这个蹩脚的定义,我们可以将其转换为更优雅的风格。我们可以运行print(config.port_to_dataconf()),它将打印出

import ubelt as ub
import scriptconfig as scfg

class MyConfig(scfg.DataConfig):
    """
    argparse CLI generated by scriptconfig 0.7.12
    """
    simple_option1 = scfg.Value(1, help=None)
    simple_option2 = scfg.Value(2, help=None)

然后使用它使重构更容易。scriptconfig程序的最后状态可能看起来像这样

import ubelt as ub
import scriptconfig as scfg

class MyConfig(scfg.DataConfig):
    """
    This is my CLI description
    """
    simple_option1 = scfg.Value(1, help=ub.paragraph(
        '''
        A reasonably detailed but concise description of an argument.
        About one paragraph is reasonable.
        ''')
    simple_option2 = scfg.Value(2, help='more help is better')

    @classmethod
    def main(cls, cmdline=1, **kwargs):
        config = cls.cli(cmdline=cmdline, data=kwargs)
        my_function(config)

def my_function(config):
    # Continued algorithmic and debugging logic
    ...

请注意,对...的基本影响(即函数的有趣部分)完全保持不变!从它的角度来看,你从未对原始的config字典做过任何事情,因为scriptconfig在每个阶段都进行了鸭子类型。

安装

scriptconfig软件包可以通过pip安装

pip install scriptconfig

要使用argcomplete和rich-argparse支持安装,要么单独安装这些软件包,要么使用

pip install scriptconfig[optional]

功能

  • 序列化为JSON

  • 字典式接口。默认情况下,Config对象在独立于配置文件或命令行的情况下运行。

  • 可以创建命令行界面

    • 可以直接创建独立的argparse对象

    • 可以使用特殊的命令行加载,使用self.load(cmdline=True)。这通过以下方式扩展了基本的argparse接口:

      • 可以选择以--option value--option=value的形式指定选项

      • 默认配置选项允许“智能”转换值,如列表和路径

      • 在通过load读取命令行时自动添加--config--dumps--dump CLI选项

  • 模糊连字符匹配:例如,--foo-bar=2--foo_bar=2对于argparse选项被处理为相同(注意:模态命令还没有此选项)

  • 继承联合配置。

  • 模态配置(见scriptconfig.modal)

  • argcomplete集成以实现shell自动完成。

  • rich_argparse集成,以实现彩色的CLI帮助页面。

示例脚本

Scriptconfig用于定义一个平面配置字典,其值可以通过Python关键字参数、命令行参数或YAML配置文件指定。考虑以下脚本,该脚本打印其配置、打开文件、计算其哈希值,然后将结果打印到stdout。

import scriptconfig as scfg
import hashlib


class FileHashConfig(scfg.DataConfig):
    """
    The docstring will be the description in the CLI help
    """
    fpath = scfg.Value(None, position=1, help='a path to a file to hash')
    hasher = scfg.Value('sha1', choices=['sha1', 'sha512'], help='a name of a hashlib hasher')


def main(**kwargs):
    config = FileHashConfig.cli(data=kwargs)
    print('config = {!r}'.format(config))
    fpath = config['fpath']
    hasher = getattr(hashlib, config['hasher'])()

    with open(fpath, 'rb') as file:
        hasher.update(file.read())

    hashstr = hasher.hexdigest()
    print('The {hasher} hash of {fpath} is {hashstr}'.format(
        hashstr=hashstr, **config))


if __name__ == '__main__':
    main()

如果此脚本在模块hash_demo.py中(例如,在此存储库的示例文件夹中),可以通过以下方式调用它。

仅从命令行

# Get help
python hash_demo.py --help

# Using key-val pairs
python hash_demo.py --fpath=$HOME/.bashrc --hasher=sha1

# Using a positional arguments and other defaults
python hash_demo.py $HOME/.bashrc

使用YAML配置从命令行

# Write out a config file
echo '{"fpath": "hashconfig.json", "hasher": "sha512"}' > hashconfig.json

# Use the special `--config` cli arg provided by scriptconfig
python hash_demo.py --config=hashconfig.json

# You can also mix and match, this overrides the hasher in the config with sha1
python hash_demo.py --config=hashconfig.json --hasher=sha1

最后,您可以像老式的Python那样调用它。

import hash_demo
hash_demo.main(fpath=hash_demo.__file__, hasher='sha512')

自动补全

如果您安装了可选的argcomplete包,您会发现按下tab键将自动补全scriptconfig CLI的已注册参数。有关详细信息,请参阅项目说明,但在标准的Linux发行版中,您可以通过以下方式启用全局补全:

pip install argcomplete
mkdir -p ~/.bash_completion.d
activate-global-python-argcomplete --dest ~/.bash_completion.d
source ~/.bash_completion.d/python-argcomplete

然后,将这些行添加到您的.bashrc

if [ -f "$HOME/.bash_completion.d/python-argcomplete" ]; then
    source ~/.bash_completion.d/python-argcomplete
fi

最后,确保您的Python脚本顶部有以下两个注释

#!/usr/bin/env python
# PYTHON_ARGCOMPLETE_OK

项目设计目标

  • 编写可以通过命令行或Python本身调用的Python程序。

  • 任何基于字典的配置系统的直接替代品。

  • 直观解析(目前正在此方面努力),理想情况下,如果可能,改进argparse。这意味着可以轻松指定简单的列表、数字、字符串和路径。

要开始使用,让我们考虑一些示例用法

>>> import scriptconfig as scfg
>>> # In its simplest incarnation, the config class specifies default values.
>>> # For each configuration parameter.
>>> class ExampleConfig(scfg.DataConfig):
>>>     num = 1
>>>     mode = 'bar'
>>>     ignore = ['baz', 'biz']
>>> # Creating an instance, starts using the defaults
>>> config = ExampleConfig()
>>> assert config['num'] == 1
>>> # Or pass in known data. (load as shown in the original example still works)
>>> kwargs = {'num': 2}
>>> config = ExampleConfig.cli(default=kwargs, cmdline=False)
>>> assert config['num'] == 2
>>> # The `load` method can also be passed a JSON/YAML file/path.
>>> config_fpath = '/tmp/foo'
>>> open(config_fpath, 'w').write('{"mode": "foo"}')
>>> config.load(config_fpath, cmdline=False)
>>> assert config['num'] == 2
>>> assert config['mode'] == "foo"
>>> # It is possbile to load only from CLI by setting cmdline=True
>>> # or by setting it to a custom sys.argv
>>> config = ExampleConfig.cli(argv=['--num=4'])
>>> assert config['num'] == 4
>>> # Note that using `config.load(cmdline=True)` will just use the
>>> # contents of sys.argv

在上面的示例中,默认字典中的键是命令行参数,值是它们的默认值。您可以通过将它们包装在scriptconfig.Value对象中来增强默认值,以封装有关帮助文档或类型信息的信息。

>>> import scriptconfig as scfg
>>> class ExampleConfig(scfg.Config):
>>>     __default__ = {
>>>         'num': scfg.Value(1, help='a number'),
>>>         'mode': scfg.Value('bar', help='mode1 help'),
>>>         'mode2': scfg.Value('bar', type=str, help='mode2 help'),
>>>         'ignore': scfg.Value(['baz', 'biz'], help='list of ignore vals'),
>>>     }
>>> config = ExampleConfig()
>>> # smartcast can handle lists as long as there are no spaces
>>> config.load(cmdline=['--ignore=spam,eggs'])
>>> assert config['ignore'] == ['spam', 'eggs']
>>> # Note that the Value type can influence how data is parsed
>>> config.load(cmdline=['--mode=spam,eggs', '--mode2=spam,eggs'])

(注意上面的示例使用的是较旧的Config用法模式,其中属性是__default__字典的成员。从版本0.6.2以后,应优先考虑使用DataConfig类。但是,如果需要包装现有的字典,始终可以使用__default__属性。)

注意事项

CLI值中的逗号

当使用scriptconfig生成命令行界面时,它使用一个名为smartcast的函数来尝试在未显式给出时确定输入类型。如果您曾经使用过试图“智能”的程序,您会知道这可能会导致一些奇怪的行为。这里发生这种情况的情况是当您传递一个包含逗号的值时。如果您没有指定默认值作为具有指定类型的scriptconfig.Value,它将解释您的输入为值的列表。在未来,我们可能会更改smartcast的行为,或者阻止它用作默认值。

布尔标志和位置参数

scriptconfig 总是提供一种键值方式来表示参数。然而,它也认识到有时你可能只想输入 --flag,而不是 --flag=1。对于具有 isflag=1Values,我们允许这样做,但这会导致位置参数的边缘情况歧义。对于以下示例

class MyConfig(scfg.DataConfig):
    arg1 = scfg.Value(None, position=1)
    flag1 = scfg.Value(False, isflag=True, position=1)

对于 --flag 1,我们无法确定你是否想要 {'arg1': 1, 'flag1': False} 还是 {'arg1': None, 'flag1': True}

可以通过以下方式解决这个问题:使用严格的关键字/值参数,在使用标志参数之前表达所有位置参数,或使用 `` – `` 结构并将所有位置参数放在末尾。将来,我们可能会在指定此类参数时引发 AmbiguityError,但现在我们将此行为留为未定义。

常见问题解答(FAQ)

问题:我该如何使用 JSON 文件覆盖 scriptconfig 对象的默认值?

答案:这取决于你是否想通过命令行传递该 JSON 文件的路径,或者你是否已经将该文件加载到内存中。有方法可以实现这两种情况。在第一种情况下,你可以传递 --config=<path-to-your-file>(假设你在创建配置对象时设置了 cmdline=True 关键字参数,例如: config = MyConfig(cmdline=True)。在第二种情况下,当你创建 scriptconfig 对象的实例时,在创建对象时传递 default=<your dict>:例如 config = MyConfig(default=json.load(open(fpath, 'r')))。但特殊的 --config --dump--dumps 命令行参数已经嵌入到脚本配置中,以简化这一过程。

待办事项

  • [ ] 嵌套模式 CLI

  • ][ ] 模型CLI中的模糊连字符

  • [X] 嵌套层次结构的策略(目前不允许)- 这里将是jsonargparse的解决方案。

    • [ ] 如何与jsonargparse最佳集成

  • [ ] 智能广播策略(目前已启用)

    • [ ] 找一种优雅的方式让智能广播做得更少。(例如,不解析列表,但整数是可以的,我们可以考虑接受YAML)

  • [X] 位置参数的策略(目前处于实验阶段)- 我们已以允许的方式实现了它们,但有一个未定义的角落案例。

    • [X] 固定长度 - 不行

    • [X] 可变长度

    • [X] argparse是否可以修改为始终允许它们出现在开头或结尾? - 可能不行。

    • [x] 我们能否让argparse允许位置参数更改前缀参数的值,同时仍然有合理的帮助菜单?

  • [x] 布尔标志的策略 - 请参阅scriptconfig.Valueisflag参数

  • [x] 改进argparse默认自动生成的帮助文档(需要探索argparse的可能性和扩展的可行性)

项目详情


下载文件

下载适用于您的平台的文件。如果您不确定要选择哪个,请了解有关安装包的更多信息。

源分布

scriptconfig-0.8.0.tar.gz (82.5 kB 查看哈希值)

上传时间

构建分布

scriptconfig-0.8.0-py3-none-any.whl (68.1 kB 查看哈希值)

上传时间 Python 3

由以下支持

AWS AWS 云计算和安全赞助商 Datadog Datadog 监控 Fastly Fastly CDN Google Google 下载分析 Microsoft Microsoft PSF 赞助商 Pingdom Pingdom 监控 Sentry Sentry 错误日志 StatusPage StatusPage 状态页面