配置

简介

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

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

配置的层次结构

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

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

  2. Collection-driven configurations defined in tasks modules via Collection.configure. (See 基于 Collection 的配置 below for details.)

    • Sub-collections’ configurations get merged into the top level collection and the final result forms the basis of the overall configuration setup.

  3. System-level configuration file stored in /etc/, such as /etc/invoke.yaml. (See Configuration files for details on this and the other config-file entries.)

  4. User-level configuration file found in the running user’s home directory, e.g. ~/.invoke.yaml.

  5. Project-level configuration file living next to your top level tasks.py. For example, if your run of Invoke loads /home/user/myproject/tasks.py (see our docs on the load process), this might be /home/user/myproject/invoke.yaml.

  6. Environment variables found in the invoking shell environment.

    • These aren’t as strongly hierarchical as the rest, nor is the shell environment namespace owned wholly by Invoke, so we must rely on slightly verbose prefixing instead - see Environment variables for details.

  7. Runtime configuration file whose path is given to -f, e.g. inv -f /random/path/to/config_file.yaml. This path may also be set via the INVOKE_RUNTIME_CONFIG env var.

  8. Command-line flags for certain core settings, such as -e.

  9. Modifications made by user code at runtime.

Default configuration values

Below is a list of all the configuration values and/or section Invoke itself uses to control behaviors such as Context.run’s echo and pty flags, task deduplication, and so forth.

备注

The storage location for these values is inside the Config class, specifically as the return value of Config.global_defaults; see its API docs for more details.

For convenience, we refer to nested setting names with a dotted syntax, so e.g. foo.bar refers to what would be (in a Python config context) {'foo': {'bar': <value here>}}. Typically, these can be read or set on Config and Context objects using attribute syntax, which looks nearly identical: c.foo.bar.

  • The tasks config tree holds settings relating to task execution.

    • tasks.dedupe controls Task deduplication and defaults to True. It can also be overridden at runtime via --no-dedupe.

    • tasks.auto_dash_names controls whether task and collection names have underscores turned to dashes on the CLI. Default: True. See also 破折号和下划线.

    • tasks.collection_name controls the Python import name sought out by collection discovery, and defaults to "tasks".

    • tasks.executor_class allows users to override the class instantiated and used for task execution.

      Must be a fully-qualified dotted path of the form module(.submodule...).class, where all but .class will be handed to importlib.import_module, and class is expected to be an attribute on that resulting module object.

      Defaults to None, meaning to use the running Program object’s executor_class attribute.

      警告

      Take care if using this setting in tandem with custom program binaries, since custom programs may specify their own default executor class (which your use of this setting will override!) and assume certain behaviors stemming from that.

    • tasks.search_root allows overriding the default collection discovery root search location. It defaults to None, which indicates to use the executing process’ current working directory.

  • The run tree controls the behavior of Runner.run. Each member of this tree (such as run.echo or run.pty) maps directly to a Runner.run keyword argument of the same name; see that method’s docstring for details on what these settings do & what their default values are.

  • The runners tree controls _which_ runner classes map to which execution contexts; if you’re using Invoke by itself, this will only tend to have a single member, runners.local. Client libraries may extend it with additional key/value pairs, such as runners.remote.

  • The sudo tree controls the behavior of Context.sudo:

    • sudo.password controls the autoresponse password submitted to sudo’s password prompt. Default: None.

      警告

      While it’s possible to store this setting, like any other, in configuration files – doing so is inherently insecure. We highly recommend filling this config value in at runtime from a secrets management system of some kind.

    • sudo.prompt holds the sudo password prompt text, which is both supplied to sudo -p, and searched for when performing auto-response. Default: [sudo] password:.

  • A top level config setting, debug, controls whether debug-level output is logged; it defaults to False.

    debug can be toggled via the -d CLI flag, which enables debugging after CLI parsing runs. It can also be toggled via the INVOKE_DEBUG environment variable which - unlike regular env vars - is honored from the start of execution and is thus useful for troubleshooting parsing and/or config loading.

  • A small config tree, timeouts, holds various kinds of timeout controls. At present, for Invoke, this only holds a command subkey, which controls subprocess execution timeouts.

    • Client code often adds more to this tree, and Invoke itself may add more in the future as well.

Configuration files

Loading

For each configuration file location mentioned in the previous section, we search for files ending in .yaml, .yml, .json or .py (in that order!), load the first one we find, and ignore any others that might exist.

For example, if Invoke is run on a system containing both /etc/invoke.yml and /etc/invoke.json, only the YAML file will be loaded. This helps keep things simple, both conceptually and in the implementation.

Format

Invoke’s configuration allows arbitrary nesting, and thus so do our config file formats. All three of the below examples result in a configuration equivalent to {'debug': True, 'run': {'echo': True}}:

  • YAML

    debug: true
    run:
        echo: true
    
  • JSON

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

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

For further details, see these languages’ own documentation.

Environment variables

Environment variables are a bit different from other configuration-setting methods, since they don’t provide a clean way to nest configuration keys, and are also implicitly shared amongst the entire system’s installed application base.

In addition, due to implementation concerns, env vars must be pre-determined by the levels below them in the config hierarchy (in other words - env vars may only be used to override existing config values). If you need Invoke to understand a FOOBAR environment variable, you must first declare a foobar setting in a configuration file or in your task collections.

Basic rules

To mitigate the shell namespace problem, we simply prefix all our env vars with INVOKE_.

Nesting is performed via underscore separation, so a setting that looks like e.g. {'run': {'echo': True}} at the Python level becomes INVOKE_RUN_ECHO=1 in a typical shell. See Nesting vs underscored names below for more on this.

Type casting

Since env vars can only be used to override existing settings, the previous value of a given setting is used as a guide in casting the strings we get back from the shell:

  • If the current value is a string or Unicode object, it is replaced with the value from the environment, with no casting whatsoever;

    • Depending on interpreter and environment, this means that a setting defaulting to a non-Unicode string type (eg a str on Python 2) may end up replaced with a Unicode string, or vice versa. This is intentional as it prevents users from accidentally limiting themselves to non-Unicode strings.

  • If the current value is None, it too is replaced with the string from the environment;

  • Booleans are set as follows: 0 and the empty value/string (e.g. SETTING=, or unset SETTING, or etc) evaluate to False, and any other value evaluates to True.

  • Lists and tuples are currently unsupported and will raise an exception;

    • In the future we may implement convenience transformations, such as splitting on commas to form a list; however since users can always perform such operations themselves, it may not be a high priority.

  • All other types - integers, longs, floats, etc - are simply used as constructors for the incoming value.

    • For example, a foobar setting whose default value is the integer 1 will run all env var inputs through int, and thus FOOBAR=5 will result in the Python value 5, not "5".

Nesting vs underscored names

Since environment variable keys are single strings, we must use some form of string parsing to allow access to nested configuration settings. As mentioned above, in basic use cases this just means using an underscore character: {'run': {'echo': True}} becomes INVOKE_RUN_ECHO=1.

However, ambiguity is introduced when the settings names themselves contain underscores: is INVOKE_FOO_BAR=baz equivalent to {'foo': {'bar': 'baz'}}, or to {'foo_bar': 'baz'}? Thankfully, because env vars can only be used to modify settings declared at the Python level or in config files, we look at the current state of the config to determine the answer.

There is still a corner case where both possible interpretations exist as valid config paths (e.g. {'foo': {'bar': 'default'}, 'foo_bar': 'otherdefault'}). In this situation, we honor the Zen of Python and refuse to guess; an error is raised instead, counseling users to modify their configuration layout or avoid using env vars for the setting in question.

基于 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 提供了额外的配置方法,根据你的需要可能是合适的。

脚注

1

复制和修改文件会破坏代码重用;重写模块级的 default_path 变量将不能很好地处理并发性;用不同的默认参数包装任务可以工作,但很脆弱,会增加样板文件。