配置

简介

invoke 提供了多方面的配置机制,通过配置文件、环境变量、任务名称空间 和 CLI 标志的层次结构,允许配置核心行为和任务的行为。

配置查找、加载、解析和合并的最终结果是 Config 对象,它的行为类似于(嵌套的)Python 字典。当它运行时调用引用此对象(确定方法的默认行为,如 Context.run),并将它作为 Context.config 暴露给用户的任务或作为 Context 上的快捷属性访问。

配置的层次结构

简而言之,配置值相互覆盖的顺序如下:

  1. 通过配置可以控制 内部默认行为值。详细信息请参见 默认配置值

  2. 通过 Collection.configure 在任务模块中定义的 集合驱动的配置。(详情请参阅下文 基于 Collection 的配置。)”

    • 子集合的配置会被合并到顶层集合中,最终结果构成整体配置设置的基础。

  3. 存储在 /etc/ 中的 系统级配置文件,例如 /etc/invoke.yaml。(有关此条目及其他配置文件条目的详细信息,请参阅 配置文件。)”

  4. 位于运行用户主目录中的 用户级配置文件,例如 ~/.invoke.yaml

  5. 位于顶层 tasks.py 旁边的 项目级配置文件。例如,如果你的 Invoke 运行加载了 /home/user/myproject/tasks.py (请参阅关于 加载过程 的文档),那么该配置文件可能是 /home/user/myproject/invoke.yaml

  6. 存在于调用 shell 环境中的 环境变量

    • 这些配置不像其他配置那样具有严格的层次结构,而且 shell 环境的命名空间并不完全由 Invoke 控制,因此必须依赖稍微冗长的前缀来区分——详情请参阅 环境变量

  7. 运行时配置文件,其路径通过 -f 指定,例如 inv -f /random/path/to/config_file.yaml。此路径也可以通过 INVOKE_RUNTIME_CONFIG 环境变量设置。

  8. 某些核心设置的 命令行标志,例如 -e

  9. 用户代码在运行时进行的修改

默认配置值

以下是 Invoke 自身使用的所有配置值和/或部分,用于控制行为如 Context.runechopty 标志、任务去重等。

备注

这些值的存储位置在 Config 类中,具体是 Config.global_defaults 的返回值;请参阅其 API 文档以获取更多详细信息。

为了方便起见,使用点语法来引用嵌套的设置名称,例如 foo.bar 指的是(在 Python 配置上下文中) {'foo': {'bar': <value here>}}。通常,这些设置可以使用属性语法在 ConfigContext 对象上读取或设置,其形式几乎完全相同:c.foo.bar

  • tasks 配置树保存与任务执行相关的设置。

    • tasks.dedupe 控制 Task deduplication,默认值为 True。也可以在运行时通过 --no-dedupe 覆盖。

    • tasks.auto_dash_names 控制 CLI 中任务和集合名称中的下划线是否转换为破折号。默认值为 True。另请参阅 破折号和下划线

    • tasks.collection_name 控制通过 collection discovery 寻找的 Python 导入名称,默认值为 "tasks"

    • tasks.executor_class 允许用户覆盖用于任务执行的实例化和使用的类。

      必须是形式 module(.submodule...).class 的完全限定路径,除了 .class 之外的所有部分都将传递给 importlib.import_module,并且 class 是该结果模块对象上的属性。

      默认值为 None,意味着使用正在运行的 Program 对象的 executor_class 属性。

      警告

      小心使用此设置,因为它与 custom program binaries 一起使用时可能会产生问题。自定义程序可能指定自己的默认执行器类(你的此设置将覆盖它!),并假设某些行为源于该类。

    • tasks.ignore_unknown_help``(默认值:``False)允许用户禁用‘为不存在的参数提供了帮助键’的错误。通常情况下,Invoke 会认为这种情况意味着 @taskhelp 参数中存在拼写错误,但有时用户有充分的理由这样做。

    • tasks.search_root 允许覆盖默认的 collection discovery 根搜索位置。默认值为 None,表示使用正在执行进程的当前工作目录。

  • run 树控制 Runner.run 的行为。此树中的每个成员(例如 run.echorun.pty)直接映射到具有相同名称的 Runner.run 关键字参数;请参阅该方法的文档字符串,了解这些设置的功能及其默认值。

  • runners 树控制哪个运行器类映射到哪个执行上下文;如果你单独使用 Invoke,这通常只会有一个成员,runners.local。客户端库可能会用额外的键/值对扩展它,例如 runners.remote

  • sudo 树控制 Context.sudo 的行为:

    • sudo.password 控制提交给 sudo 密码提示的自动响应密码。默认值为 None

      警告

      虽然可以像其他设置一样将此设置存储在 配置文件 中——但这样做本质上是不安全的。强烈建议在运行时通过某种密钥管理系统来填充此配置值。

    • sudo.prompt 保存了 sudo 密码提示文本,该文本既会传递给 sudo -p,也会在执行 自动响应 时被搜索。默认值为 [sudo] password:

  • 顶层配置设置 debug 控制是否记录调试级别的输出;其默认值为 False

    debug 可以通过 -d CLI 标志切换,该标志在 CLI 解析完成后启用调试。它也可以通过 INVOKE_DEBUG 环境变量切换——与常规环境变量不同,该变量从执行开始时即生效,因此可用于排查解析和/或配置加载问题。

  • 小的配置树 timeouts 保存了各种超时控制。目前,对于 Invoke 来说,它仅包含 command 子键,用于控制子进程执行的超时时间。

    • 客户端代码经常向此树添加更多内容,Invoke 本身也可能会在未来添加更多内容。

配置文件

加载

对于前面提到的每个配置文件位置,会搜索以 .yaml.yml.json.py 结尾的文件(按此顺序!),加载找到的第一个文件,并忽略可能存在的其他文件。

例如,如果在包含 /etc/invoke.yml /etc/invoke.json 的系统上运行 Invoke,只会加载 YAML 文件。这有助于在概念和实现上保持简洁。

格式

Invoke 的配置允许任意嵌套,因此配置文件格式也允许任意嵌套。以下三个示例都将产生与 {'debug': True, 'run': {'echo': True}} 等效的配置:

  • YAML

    debug: true
    run:
        echo: true
    
  • JSON

    {
        "debug": true,
        "run": {
            "echo": true
        }
    }
    
  • Python:

    debug = True
    run = {
        "echo": True
    }
    

有关这些语言的详细信息,请参阅它们各自的文档。

环境变量

环境变量与其他配置设置方法有所不同,因为它们无法提供一种干净的方式来嵌套配置键,并且它们也隐含地在整个系统的已安装应用程序基中共享。

此外,由于实现上的考虑,环境变量必须由配置层次结构中位于它们下方的层级预先确定(换句话说——环境变量只能用于覆盖现有的配置值)。如果你需要 Invoke 理解 FOOBAR 环境变量,你必须首先在配置文件或任务集合中声明一个 foobar 设置。

基本规则

为了避免 shell 命名空间问题,简单地将所有环境变量前缀为 INVOKE_

嵌套通过下划线分隔符进行,因此像 {'run': {'echo': True}} 这样的 Python 级别设置在典型的 shell 中变为 INVOKE_RUN_ECHO=1。更多信息请参见 嵌套 vs 下划线命名

类型转换

由于环境变量只能用于覆盖现有设置,因此给定设置的先前值会被用作指导,以将从 shell 获取的字符串转换为相应的类型:

  • 如果当前值是 Unicode 字符串,则直接替换为环境变量中的值,不进行任何类型转换;

  • 如果当前值为 None,它也会用环境变量中的字符串替换。

  • 布尔值的设置如下:0 和空值/空字符串(例如 SETTING=,或 unset SETTING,或其他类似情况)会被评估为 False,而任何其他值会被评估为 True

  • 列表和元组当前不受支持,会引发异常。

    • 在未来可能会实现一些便利的转换,例如使用逗号拆分以形成列表;但由于用户可以随时执行这些操作,因此它可能不是高优先级。

  • 所有其他类型(整数、长整数、浮点数等)都用作传入值的构造函数。

    • 例如,默认值为整数 1foobar 设置会通过 int 处理所有环境变量输入,因此 FOOBAR=5 将导致 Python 值为 5,而不是 "5"

嵌套 vs 下划线命名

由于环境变量键是单个字符串,因此必须使用某种字符串解析来允许访问嵌套的配置设置。如上所述,在基本用例中这仅仅意味着使用下划线字符:{'run': {'echo': True}} 变成 INVOKE_RUN_ECHO=1

然而,当设置名称本身包含下划线时,会引入歧义:INVOKE_FOO_BAR=baz 是等同于 {'foo': {'bar': 'baz'}},还是等同于 {'foo_bar': 'baz'}?幸运的是,由于环境变量只能用于修改在 Python 级别或配置文件中声明的设置,可以通过查看配置的当前状态来确定答案。

仍然存在一种极端情况,即*两种*可能的解释都作为有效的配置路径存在(例如 {'foo': {'bar': 'default'}, 'foo_bar': 'otherdefault'})。在这种情况下,我们遵循 Python 之禅,拒绝猜测;相反,会引发错误,建议用户修改其配置布局或避免使用环境变量来设置相关值。

基于 Collection 的配置

Collection 对象可以包含配置映射,通过 Collection.configure 来设置,并且(按照 层次结构)这通常形成了系统中最低级别的配置。

当集合是 嵌套 时,配置默认是 ‘向下’ 合并:当冲突出现时,相对于靠近被调用任务的内部命名空间,靠近根的外部命名空间将获胜。

备注

这里的“内部”任务是指从根目录到被调用任务所在目录的路径上的任务。’Sibling’ 子集合将被忽略。

举个简单的例子

from invoke import Collection, task

# This task & collection could just as easily come from
# another module somewhere.
@task
def mytask(c):
    print(c['conflicted'])
inner = Collection('inner', mytask)
inner.configure({'conflicted': 'default value'})

# Our project's root namespace.
ns = Collection(inner)
ns.configure({'conflicted': 'override value'})

调用 inner.mytask 的结果

$ inv inner.mytask
override value

真实世界配置使用的例子

前面的章节中有一些小例子;本节提供了一组看起来更真实的例子,展示了配置系统是如何工作的。

设置

从硬编码其值的半现实的任务开始,然后使用各种配置机制进行构建。用于构建 Sphinx docs 的小模块可以这样开始

from invoke import task

@task
def clean(c):
    c.run("rm -rf docs/_build")

@task
def build(c):
    c.run("sphinx-build docs docs/_build")

然后重构构建目标

target = "docs/_build"

@task
def clean(c):
    c.run("rm -rf {}".format(target))

@task
def build(c):
    c.run("sphinx-build docs {}".format(target))

还可以允许运行时参数化

default_target = "docs/_build"

@task
def clean(c, target=default_target):
    c.run("rm -rf {}".format(target))

@task
def build(c, target=default_target):
    c.run("sphinx-build docs {}".format(target))

这个任务模块只适用于一组用户,但如果我们希望允许重用呢?有些人可能希望将此模块与另一个默认目标一起使用。使用配置数据(通过上下文参数提供)来配置这些设置通常是更好的解决方案。[1]

任务 collection 配置

配置 settinggetting 允许将其他的‘硬编码’默认值移动到配置结构中,下游用户可以自由地重新定义。让我们把这个应用到我们的例子中。首先,我们添加一个显式的命名空间对象

from invoke import Collection, task

default_target = "docs/_build"

@task
def clean(c, target=default_target):
    c.run("rm -rf {}".format(target))

@task
def build(c, target=default_target):
    c.run("sphinx-build docs {}".format(target))

ns = Collection(clean, build)

然后,可以将默认构建目标值移到集合的默认配置中,并通过上下文引用它。在这一点上,也将 kwarg 的默认值改为 None,这样就可以确定是否给出了一个运行时值。结果

@task
def clean(c, target=None):
    if target is None:
        target = c.sphinx.target
    c.run("rm -rf {}".format(target))

@task
def build(c, target=None):
    if target is None:
        target = c.sphinx.target
    c.run("sphinx-build docs {}".format(target))

ns = Collection(clean, build)
ns.configure({'sphinx': {'target': "docs/_build"}})

结果并不比我们开始时更复杂,正如我们接下来将看到的,现在用户以各种方式重写默认值是微不足道的。

配置覆盖

当然,最低级别的覆盖只是修改本地 Collection 树,其中导入了分布式模块。例如,如果上面的模块是作为 myproject.docs 分发的,可以定义 tasks.py 来做这个

from invoke import Collection, task
from myproject import docs

@task
def mylocaltask(c):
    # Some local stuff goes here
    pass

# Add 'docs' to our local root namespace, plus our own task
ns = Collection(mylocaltask, docs)

然后把这个加到下面

# Our docs live in 'built_docs', not 'docs/_build'
ns.configure({'sphinx': {'target': "built_docs"}})

现在有了 docs 子命名空间,它的构建目标默认为 built_docs,而不是 docs/_build。运行时用户仍然可以通过旗标(例如 inv docs.build --target='some/other/dir')像以前一样构建。

如果你更喜欢配置文件而不是在 python 中调整你的命名空间树,这也可以;不要把上面的代码添加到前面的代码片段中,而是把它放到 tasks.py 旁边的名为 invoke.yaml 的文件中

sphinx:
    target: built_docs

在这个例子中,那种 local-to-project 的配置文件是最有意义的,但是不要忘记 config hierarchy 提供了额外的配置方法,根据你的需要可能是合适的。

脚注