在Python中加载配置文件
#python #置 #yaml #attrs

配置文件无处不在。您的应用程序可能需要拥有一个:

有很多原因。
  • 您有要在重新启动之外持续存在的配置。

  • 您的配置代表物理状态;例如,它包含外围设备的设置,用于完成任务的存储过程,或者也许表达了实时用户界面的布局。

  • 您的应用程序的配置无法轻松表示为一系列变量。 CI管道,工作流程等。具有许多复杂的嵌套,重复的块,甚至内部链接。

  • 您希望该应用程序能够坚持自己对配置的更改,例如更改Windows尺寸,菜单设置或凭据。在这种情况下,配置文件比用户写的内容更多地充当数据库。

在所有这些情况下,配置的结构非常重要且可能长期存在。您的配置语法中的错误将很难撤消,因此有一个计划预期,并进行设计以扩展和记录。

在本文中,我们将学习如何以干净,易于支持且易于扩展的方式加载YAML配置文件。我们将通过创建自己的YAML任务自动化语法来做到这一点,我们将调用 taskbook 文件:

# taskbook.yml

group: # name of group

tasks: # list of tasks
  - name: # name of task
    module: # module to use
    options:
      # key / value options

  # ...

我们将编写一个程序来阅读它们,我们将其称为 taskable *。完成后,很容易确定支持的字段,安全验证配置值,为将来的需求添加更多字段,甚至在我们程序中以属性为属性。

*Ansible playbook syntax的任何相似之处,真实或想象中的纯粹是偶然的。ð

创建命令行工具

让我们创建一个名为taskable.py的文件,以包含我们的任务实现:

# taskable.py
import argparse

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("file", type=argparse.FileType("r"))
    args = parser.parse_args()

if __name__ == " __main__":
    main()

这为argparse命令行界面提供了脚手架(有关更多信息,请参见我们的article on Python CLIs)。

您可以按以下方式运行脚本:

python3 taskable.py
$ python3 taskable.py
usage: taskable.py [-h] file
taskable.py: error: the following arguments are required: file

要能够在文件中读取,我们需要先创建文件,下一步将进行。

创建一个任务簿文件

我将使用YAML作为配置文件,因为它易于阅读并且对此我很满意,但是您可以轻松支持JSON或TOML,因为它们提供了类似的API。

创建一个taskbook.yml文件并添加以下内容:

# taskbook.yml
group: localhost
tasks:
  - name: copy file.txt to the place
    module: saucy.copy
    options:
      source: file.txt
      dest: /etc/file.txt

  - name: install a package
    module: cheesy.package
    options:
      name:
        - fzf
        - tree
      upgrade: true

  - name: enable the service
    module: lettuce.service
    options:
      enable: true
      start: true

此时,我们将能够运行以下内容:

python3 taskable.py taskbook.yaml

但是,什么都不会发生,因为我们的应用程序还没有打印任何东西。

在yaml文件中阅读

yaml文件很容易使用Python读取。有多个库可用,但是koude3是事实上的标准,通常安装在您已经使用的任何系统上。

如果您没有pyyaml(或者因为很棒而使用虚拟环境),请立即安装:

pip install pyyaml

然后,在您的taskable.py文件中,导入yaml包并在yaml文件中阅读:

import yaml
...
data = yaml.safe_load(args.file)

到目前为止我们的taskable.py文件:

# taskable.py
import argparse
import yaml

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("file", type=argparse.FileType("r"))
    args = parser.parse_args()
    data = yaml.safe_load(args.file)

if __name__ == " __main__":
    main()

此时,您将能够在YAML文件中读取,但是目前尚无输出。我们可以在这里停下来,并以嵌套词典和数组的形式访问其值,例如:

data["tasks"][0]["module"]

...但是有几个问题。

首先,根本没有验证,因此畸形的配置文件具有无法预测的结果。其次,字符串是不透明的数据,因此IDE自动完成将无法使用。更改字段名称将需要手动搜索该代码。我希望您永远不会拼写字段名。

不,我们可以做得更好,我们将在下一部分中构建数据模型开始。

创建数据模型

我们需要一种表达数据格式的方法,使其功能正常。为此,我更喜欢使用koude8,该koude8可以为我们提供数据验证,使我们的课程更具性能,使我们能够作为具有较少样板的属性访问我们的字段,而更多。

让我们安装attrs

pip install attrs

然后将以下内容添加到您的taskable.py文件:

from typing import Any
...
from attrs import define, field

@define
class Task:
    name: str
    module: str
    options: dict[str, Any] = field(factory=dict)

@define
class Taskbook:
    group: str
    tasks: list[Task]

到目前为止我们的taskable.py文件:

# taskable.py
import argparse
from typing import Any

import yaml
from attrs import define, field

@define
class Task:
    name: str
    module: str
    options: dict[str, Any] = field(factory=dict)

@define
class Taskbook:
    group: str
    tasks: list[Task]

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("file", type=argparse.FileType("r"))
    args = parser.parse_args()
    data = yaml.safe_load(args.file)

if __name__ == " __main__":
    main()

这两个类TaskTaskbook完全表达了任务簿格式。不过,我们不会自己实例化它们,因为我们将在下一部分中学习一种自动执行的方法。

结构化成模型

“ structurize”是一个$ 6的单词(我可能已经构成的),它转化为“将所有数据加载到精美的模型类中”。我之所以使用它,是因为“去序列化”听起来很糟糕,并且很难输入。 ð

将YAML数据构造到attrs类中的最简单方法是使用koude15软件包。最简单的用法看起来像:

import cattrs
taskbook = cattrs.structure(data, Taskbook)

让我们将其添加到我们的taskable.py文件中:

# taskable.py
import argparse
from typing import Any

import cattrs
import yaml
from attrs import define, field

@define
class Task:
    name: str
    module: str
    options: dict[str, Any] = field(factory=dict)

@define
class Taskbook:
    group: str
    tasks: list[Task]

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("file", type=argparse.FileType("r"))
    args = parser.parse_args()
    data = yaml.safe_load(args.file)
    taskbook = cattrs.structure(data, Taskbook)

if __name__ == " __main__":
    main()

这就是您所需要的! cattrs仅在获得预期的顶级类之后,将数据加载到attrs类中。

如果您需要调整行为,cattrs提供了hook mechanism。有点麻烦,但是比从头开始编写所有结构化代码要容易。

在下一部分中,我们将致力于对我们的数据做有用的事情。

使用数据

在这一点上,我们已经将数据完全构成了类,这意味着我们可以这样访问我们的配置数据:

taskbook.tasks[0].module

这使我们的代码 更易于阅读和使用。现在,我们将尝试使用它来做事。

“运行”任务

如果无法运行任务,我们的脚本有什么好处?让我们添加一些东西来模拟“运行”我们的假设任务,通过将以下内容添加到我们的taskable.pyfile:

...
print("group", taskbook.group)
for task in taskbook.tasks:
    print(f"run {task.module}: {task.name}")
...

到目前为止我们的taskable.py文件:

# taskable.py
import argparse
from typing import Any

import cattrs
import yaml
from attrs import define, field

@define
class Task:
    name: str
    module: str
    options: dict[str, Any] = field(factory=dict)

@define
class Taskbook:
    group: str
    tasks: list[Task]

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("file", type=argparse.FileType("r"))
    args = parser.parse_args()
    data = yaml.safe_load(args.file)
    taskbook = cattrs.structure(data, Taskbook)
    print("group", taskbook.group)
    for task in taskbook.tasks:
        print(f"run {task.module}: {task.name}")

if __name__ == " __main__":
    main()

运行我们的假设任务将输出以下内容:

python3 taskable.py taskbook.yml
$ python3 taskable.py taskbook.yml
group localhost
run saucy.copy: copy file.txt to the place
run cheesy.package: install a package
run lettuce.service: enable the service

不难想象将此骨架连接到真实的模块实现以驱动真实的任务执行。

列表使用的模块

也许我们想检查我们的任务簿,以找出其使用的模块。例如,在运行我们的任务之前安装必要的模块将很有用。

让我们添加一个-l / --list选项以列出使用的模块并在不运行任务的情况下退出:

...
parser.add_argument("-l", "--list", action="store_true")
...
if args.list:
    used_modules = sorted(list(set(task.module for task in taskbook.tasks)))
    for module in used_modules:
        print(module)
    return
...

到目前为止我们的taskable.py文件:

# taskable.py
import argparse
from typing import Any

import cattrs
import yaml
from attrs import define, field

@define
class Task:
    name: str
    module: str
    options: dict[str, Any] = field(factory=dict)

@define
class Taskbook:
    group: str
    tasks: list[Task]

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("-l", "--list", action="store_true")
    parser.add_argument("file", type=argparse.FileType("r"))
    args = parser.parse_args()
    data = yaml.safe_load(args.file)
    taskbook = cattrs.structure(data, Taskbook)
    if args.list:
        used_modules = sorted(list(set(task.module for task in taskbook.tasks)))
        for module in used_modules:
            print(module)
        return
    print("group", taskbook.group)
    for task in taskbook.tasks:
        print(f"run {task.module}: {task.name}")

if __name__ == " __main__":
    main()

启用了列表模式的运行taskable.py

python3 taskable.py -l taskbook.yaml
$ python3 taskable.py -l taskbook.yaml
cheesy.package
lettuce.service
saucy.copy

woot!静态分析!并且很容易实施,因为我们的数据模型定义得很好。

概括

在本教程中,我们建立了一种多功能配置加载机制。

此设置对小命令行实用程序的运作效果也同样好,与大型且复杂的数据格式化文件(如任务工作流,规格等)相同。您可以通过添加新的字段和新数据模型来继续增长应用程序,并避免从混乱的早期配置实施中产生的恶性技术债务。

最好的部分?您的配置将是稳定,并为您的应用程序和将来提供您的应用程序的基础和基础。用Eric S. Raymond的话:

智能数据结构和愚蠢的代码的效果要比相反。

- 1 Eric S. Raymond

保持聪明,人们! ð