配置¶
简介¶
invoke 提供了多方面的配置机制,通过配置文件、环境变量、任务名称空间 和 CLI 标志的层次结构,允许配置核心行为和任务的行为。
配置查找、加载、解析和合并的最终结果是 Config
对象,它的行为类似于(嵌套的)Python 字典。当它运行时调用引用此对象(确定方法的默认行为,如 Context.run
),并将它作为 Context.config
暴露给用户的任务或作为 Context
上的快捷属性访问。
配置的层次结构¶
简而言之,配置值相互覆盖的顺序如下:
通过配置可以控制 内部默认行为值。详细信息请参见 默认配置值。
通过
Collection.configure
在任务模块中定义的 集合驱动的配置。(详情请参阅下文 基于 Collection 的配置。)”子集合的配置会被合并到顶层集合中,最终结果构成整体配置设置的基础。
存储在
/etc/
中的 系统级配置文件,例如/etc/invoke.yaml
。(有关此条目及其他配置文件条目的详细信息,请参阅 配置文件。)”位于运行用户主目录中的 用户级配置文件,例如
~/.invoke.yaml
。位于顶层
tasks.py
旁边的 项目级配置文件。例如,如果你的 Invoke 运行加载了/home/user/myproject/tasks.py
(请参阅关于 加载过程 的文档),那么该配置文件可能是/home/user/myproject/invoke.yaml
。存在于调用 shell 环境中的 环境变量。
这些配置不像其他配置那样具有严格的层次结构,而且 shell 环境的命名空间并不完全由 Invoke 控制,因此必须依赖稍微冗长的前缀来区分——详情请参阅 环境变量。
运行时配置文件,其路径通过
-f
指定,例如inv -f /random/path/to/config_file.yaml
。此路径也可以通过INVOKE_RUNTIME_CONFIG
环境变量设置。某些核心设置的 命令行标志,例如
-e
。用户代码在运行时进行的修改。
默认配置值¶
以下是 Invoke 自身使用的所有配置值和/或部分,用于控制行为如 Context.run
的 echo
和 pty
标志、任务去重等。
备注
这些值的存储位置在 Config
类中,具体是 Config.global_defaults
的返回值;请参阅其 API 文档以获取更多详细信息。
为了方便起见,使用点语法来引用嵌套的设置名称,例如 foo.bar
指的是(在 Python 配置上下文中) {'foo': {'bar': <value here>}}
。通常,这些设置可以使用属性语法在 Config
和 Context
对象上读取或设置,其形式几乎完全相同: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 会认为这种情况意味着@task
的help
参数中存在拼写错误,但有时用户有充分的理由这样做。tasks.search_root
允许覆盖默认的 collection discovery 根搜索位置。默认值为None
,表示使用正在执行进程的当前工作目录。
run
树控制Runner.run
的行为。此树中的每个成员(例如run.echo
或run.pty
)直接映射到具有相同名称的Runner.run
关键字参数;请参阅该方法的文档字符串,了解这些设置的功能及其默认值。runners
树控制哪个运行器类映射到哪个执行上下文;如果你单独使用 Invoke,这通常只会有一个成员,runners.local
。客户端库可能会用额外的键/值对扩展它,例如runners.remote
。sudo
树控制Context.sudo
的行为:顶层配置设置
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
。列表和元组当前不受支持,会引发异常。
在未来可能会实现一些便利的转换,例如使用逗号拆分以形成列表;但由于用户可以随时执行这些操作,因此它可能不是高优先级。
所有其他类型(整数、长整数、浮点数等)都用作传入值的构造函数。
例如,默认值为整数
1
的foobar
设置会通过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 配置¶
配置 setting
和 getting
允许将其他的‘硬编码’默认值移动到配置结构中,下游用户可以自由地重新定义。让我们把这个应用到我们的例子中。首先,我们添加一个显式的命名空间对象
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 提供了额外的配置方法,根据你的需要可能是合适的。
脚注